Compare commits
No commits in common. 'main' and 'Dashfix' have entirely different histories.
41 changed files with 294 additions and 3079 deletions
Binary file not shown.
@ -1,17 +0,0 @@ |
|||||||
[gd_resource type="Resource" script_class="WeaponData" load_steps=3 format=3 uid="uid://b2q62xc0jw4w3"] |
|
||||||
|
|
||||||
[ext_resource type="PackedScene" uid="uid://cehc5ckhq2byd" path="res://level/scenes/weapons/Applecoremesh.tscn" id="1_1ytxi"] |
|
||||||
[ext_resource type="Script" uid="uid://d2homvlmrg6xs" path="res://level/scripts/weapon_data.gd" id="2_hfi3c"] |
|
||||||
|
|
||||||
[resource] |
|
||||||
script = ExtResource("2_hfi3c") |
|
||||||
weapon_name = "Apple Sword" |
|
||||||
description = "yum" |
|
||||||
damage = 20.0 |
|
||||||
attack_range = 3.5 |
|
||||||
attack_cooldown = 0.6 |
|
||||||
knockback_force = 12.0 |
|
||||||
startup_time = 0.2 |
|
||||||
active_time = 1.0 |
|
||||||
mesh_scene = ExtResource("1_1ytxi") |
|
||||||
weight = 2.0 |
|
||||||
@ -1,18 +0,0 @@ |
|||||||
[gd_resource type="Resource" script_class="WeaponData" load_steps=3 format=3 uid="uid://dyae861vxd8it"] |
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://d2homvlmrg6xs" path="res://level/scripts/weapon_data.gd" id="1"] |
|
||||||
[ext_resource type="PackedScene" uid="uid://cq8r5mkn3wvxj" path="res://level/scenes/weapons/LobsterAxeMesh.tscn" id="2"] |
|
||||||
|
|
||||||
[resource] |
|
||||||
script = ExtResource("1") |
|
||||||
weapon_name = "Lobster Axe" |
|
||||||
description = "A heavy-hitting axe shaped like a lobster claw. Surprisingly quick for its size." |
|
||||||
hand_type = 2 |
|
||||||
damage = 18.0 |
|
||||||
attack_cooldown = 0.7 |
|
||||||
attack_animation = "Attack_TwoHandSwing" |
|
||||||
knockback_force = 14.0 |
|
||||||
startup_time = 0.2 |
|
||||||
active_time = 1.0 |
|
||||||
mesh_scene = ExtResource("2") |
|
||||||
weight = 2.5 |
|
||||||
@ -1,124 +0,0 @@ |
|||||||
[gd_scene load_steps=5 format=3] |
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://level/scripts/armed_enemy.gd" id="1_armed_enemy"] |
|
||||||
[ext_resource type="PackedScene" uid="uid://b22ou40sbkavj" path="res://assets/characters/player/LilguyRigged.glb" id="2_lilguy"] |
|
||||||
[ext_resource type="Script" uid="uid://cf7jky1bcs560" path="res://level/scripts/lilguy_body.gd" id="3_body"] |
|
||||||
[ext_resource type="Script" uid="uid://bj3uepduxvgju" path="res://level/scripts/hurt_box.gd" id="4_hurtbox"] |
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"] |
|
||||||
radius = 0.35796 |
|
||||||
height = 1.73092 |
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_hurtbox"] |
|
||||||
radius = 0.4 |
|
||||||
height = 1.8 |
|
||||||
|
|
||||||
[node name="ArmedEnemy" type="CharacterBody3D" node_paths=PackedStringArray("_body", "_weapon_attachment", "_weapon_container", "_offhand_attachment", "_offhand_container")] |
|
||||||
collision_mask = 3 |
|
||||||
script = ExtResource("1_armed_enemy") |
|
||||||
_body = NodePath("LilguyRigged/Armature") |
|
||||||
_weapon_attachment = NodePath("LilguyRigged/Armature/Skeleton3D/WeaponPoint") |
|
||||||
_weapon_container = NodePath("LilguyRigged/Armature/Skeleton3D/WeaponPoint/WeaponContainer") |
|
||||||
_offhand_attachment = NodePath("LilguyRigged/Armature/Skeleton3D/OffhandPoint") |
|
||||||
_offhand_container = NodePath("LilguyRigged/Armature/Skeleton3D/OffhandPoint/OffhandContainer") |
|
||||||
move_speed = 4.0 |
|
||||||
detection_range = 100.0 |
|
||||||
max_health = 50.0 |
|
||||||
respawn_delay = 10.0 |
|
||||||
|
|
||||||
[node name="LilguyRigged" parent="." instance=ExtResource("2_lilguy")] |
|
||||||
|
|
||||||
[node name="Armature" parent="LilguyRigged" index="0" node_paths=PackedStringArray("_character", "animation_player")] |
|
||||||
transform = Transform3D(0.003, 0, 0, 0, -1.3113416e-10, -0.003, 0, 0.003, -1.3113416e-10, 0, 0, 0) |
|
||||||
script = ExtResource("3_body") |
|
||||||
_character = NodePath("../..") |
|
||||||
animation_player = NodePath("../AnimationPlayer") |
|
||||||
|
|
||||||
[node name="Skeleton3D" parent="LilguyRigged/Armature" index="0"] |
|
||||||
bones/0/position = Vector3(-0.32859802, 2.9141626, -546.76843) |
|
||||||
bones/0/rotation = Quaternion(-0.6608289, 0.28933656, -0.19178493, 0.66543835) |
|
||||||
bones/1/position = Vector3(0.054167695, 63.219894, -3.33786e-06) |
|
||||||
bones/1/rotation = Quaternion(0.015321612, 0.025352655, 0.0947179, 0.99506336) |
|
||||||
bones/2/position = Vector3(-1.8112361e-05, 73.7566, -1.621247e-05) |
|
||||||
bones/2/rotation = Quaternion(0.034659874, 0.050472155, 0.051301125, 0.99680465) |
|
||||||
bones/3/position = Vector3(-3.0510128e-05, 84.29319, 9.059899e-06) |
|
||||||
bones/3/rotation = Quaternion(0.029244598, 0.05379088, -0.051849354, 0.99677634) |
|
||||||
bones/4/position = Vector3(3.8038404e-05, 94.83001, 1.9073414e-06) |
|
||||||
bones/4/rotation = Quaternion(0.0006217413, 0.08164966, 0.020404326, 0.9964521) |
|
||||||
bones/5/position = Vector3(-0.25257444, 72.84532, -7.644296e-06) |
|
||||||
bones/5/rotation = Quaternion(0.037355006, 0.19943582, -0.04159446, 0.9783148) |
|
||||||
bones/6/position = Vector3(-0.606337, 174.89494, 7.152558e-06) |
|
||||||
bones/7/position = Vector3(-0.19949026, 76.75483, 52.286175) |
|
||||||
bones/7/rotation = Quaternion(0.80360717, -0.09628771, 0.10672592, 0.57754105) |
|
||||||
bones/8/position = Vector3(4.5403274e-05, 110.91907, 9.404198e-05) |
|
||||||
bones/8/rotation = Quaternion(0.25522023, -0.08967148, 0.029356971, 0.9622681) |
|
||||||
bones/9/position = Vector3(2.3064584e-05, 173.66367, 5.063071e-05) |
|
||||||
bones/9/rotation = Quaternion(0.08784258, -0.16096693, 0.24338366, 0.95243776) |
|
||||||
bones/10/position = Vector3(-2.2947788e-05, 166.48767, -1.2734416e-05) |
|
||||||
bones/11/position = Vector3(0.23053212, 76.75536, -52.28617) |
|
||||||
bones/11/rotation = Quaternion(0.14271267, -0.5852636, 0.7822879, 0.15851) |
|
||||||
bones/12/position = Vector3(1.532285e-05, 110.91911, 4.0430357e-05) |
|
||||||
bones/12/rotation = Quaternion(0.32197043, 0.13412262, 0.2711233, 0.89712787) |
|
||||||
bones/13/position = Vector3(1.5523525e-05, 173.6661, 0.00010698747) |
|
||||||
bones/13/rotation = Quaternion(0.090376236, 0.10155637, -0.39819276, 0.90717196) |
|
||||||
bones/14/position = Vector3(-2.0682812e-05, 166.48976, 3.939679e-05) |
|
||||||
bones/15/position = Vector3(0.6496186, -35.1185, 49.84838) |
|
||||||
bones/15/rotation = Quaternion(0.38543156, 0.16380574, 0.82174975, 0.38644233) |
|
||||||
bones/16/position = Vector3(8.771768e-06, 312.91962, 7.4840264e-06) |
|
||||||
bones/16/rotation = Quaternion(-0.053004134, 0.17209636, 0.39056766, 0.90279037) |
|
||||||
bones/17/position = Vector3(-1.8137518e-05, 301.05597, -2.1670077e-05) |
|
||||||
bones/17/rotation = Quaternion(0.2498236, 0.64725155, -0.67117685, 0.26110402) |
|
||||||
bones/18/position = Vector3(-3.026353e-05, 14.185886, -1.4917823e-06) |
|
||||||
bones/18/rotation = Quaternion(0.11539694, 0.017187497, -0.010002339, 0.9931204) |
|
||||||
bones/19/position = Vector3(-4.351055e-06, 11.391233, -2.5032205e-06) |
|
||||||
bones/20/position = Vector3(0.014209064, -35.118507, -49.848385) |
|
||||||
bones/20/rotation = Quaternion(-0.07370261, -0.18747209, 0.94122386, 0.27114522) |
|
||||||
bones/21/position = Vector3(2.8756085e-05, 312.91974, 5.14377e-06) |
|
||||||
bones/21/rotation = Quaternion(-0.037353504, -0.04220154, 0.46134973, 0.88542664) |
|
||||||
bones/22/position = Vector3(2.2092872e-05, 301.0575, 1.8114511e-05) |
|
||||||
bones/22/rotation = Quaternion(0.79332, 0.1285891, -0.36227074, 0.47208923) |
|
||||||
bones/23/position = Vector3(1.3624241e-05, 15.034077, 9.790485e-06) |
|
||||||
bones/23/rotation = Quaternion(0.11885707, 0.009522018, -0.0077985795, 0.9928351) |
|
||||||
bones/24/position = Vector3(-2.4847686e-06, 11.913359, -6.198885e-06) |
|
||||||
|
|
||||||
[node name="WeaponPoint" type="BoneAttachment3D" parent="LilguyRigged/Armature/Skeleton3D" index="1"] |
|
||||||
transform = Transform3D(-0.4329258, -0.61284786, 0.6610537, 0.7782944, 0.11585927, 0.6171174, -0.45478824, 0.78166056, 0.42681772, -352.385, -73.56995, -531.9614) |
|
||||||
bone_name = "mixamorig_RightHand" |
|
||||||
bone_idx = 14 |
|
||||||
|
|
||||||
[node name="WeaponContainer" type="Node3D" parent="LilguyRigged/Armature/Skeleton3D/WeaponPoint"] |
|
||||||
transform = Transform3D(36.6912, 297.2667, 16.921356, 46.72698, 11.0892515, -296.13126, -294.05847, 38.85366, -44.94499, 24.08223, -7.4241333, 7.098694) |
|
||||||
|
|
||||||
[node name="OffhandPoint" type="BoneAttachment3D" parent="LilguyRigged/Armature/Skeleton3D" index="2"] |
|
||||||
transform = Transform3D(0.62123704, -0.004605159, -0.7836091, -0.62031674, 0.6081372, -0.49535444, 0.4788229, 0.79381835, 0.3749406, 135.65929, 334.35745, -511.27094) |
|
||||||
bone_name = "mixamorig_LeftHand" |
|
||||||
bone_idx = 10 |
|
||||||
|
|
||||||
[node name="OffhandContainer" type="Node3D" parent="LilguyRigged/Armature/Skeleton3D/OffhandPoint"] |
|
||||||
transform = Transform3D(-17.74905, -295.46814, -48.82108, 21.019196, -50.01525, 295.05362, -298.73593, 14.035805, 23.660797, 0.005859375, 0.39337158, 0.06616211) |
|
||||||
|
|
||||||
[node name="AnimationPlayer" parent="LilguyRigged" index="1"] |
|
||||||
speed_scale = 2.0 |
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."] |
|
||||||
transform = Transform3D(2, 0, 0, 0, 2, 0, 0, 0, 2, -0.066, 1.647685, 0.01) |
|
||||||
shape = SubResource("CapsuleShape3D_body") |
|
||||||
|
|
||||||
[node name="HurtBox" type="Area3D" parent="." node_paths=PackedStringArray("owner_entity")] |
|
||||||
collision_layer = 16 |
|
||||||
collision_mask = 0 |
|
||||||
script = ExtResource("4_hurtbox") |
|
||||||
owner_entity = NodePath("..") |
|
||||||
|
|
||||||
[node name="HurtBoxShape" type="CollisionShape3D" parent="HurtBox"] |
|
||||||
transform = Transform3D(1.9228287, 0, 0, 0, 1.4454772, 0, 0, 0, 1.4906956, -0.066, 2.0836046, 0.01) |
|
||||||
shape = SubResource("CapsuleShape3D_hurtbox") |
|
||||||
|
|
||||||
[node name="EnemyLabel" type="Label3D" parent="."] |
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4.2, 0) |
|
||||||
billboard = 1 |
|
||||||
modulate = Color(1, 0.3, 0.3, 1) |
|
||||||
outline_modulate = Color(0, 0, 0, 0.4) |
|
||||||
text = "Armed Enemy" |
|
||||||
|
|
||||||
[editable path="LilguyRigged"] |
|
||||||
@ -1,63 +0,0 @@ |
|||||||
[gd_scene load_steps=8 format=3 uid="uid://byknup31d2b53"] |
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://cd87rsuiqhdav" path="res://level/scripts/basic_enemy.gd" id="1_basic_enemy"] |
|
||||||
[ext_resource type="Script" uid="uid://bj3uepduxvgju" path="res://level/scripts/hurt_box.gd" id="2_hurtbox"] |
|
||||||
[ext_resource type="ArrayMesh" uid="uid://dy0xld0fpulmk" path="res://assets/characters/Lobster/10029_Lobster_v1_iterations-2.obj" id="2_obxet"] |
|
||||||
[ext_resource type="Script" uid="uid://jyas86y3f0jp" path="res://level/scripts/hit_box.gd" id="3_hitbox"] |
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"] |
|
||||||
height = 1.869873 |
|
||||||
|
|
||||||
[sub_resource type="SphereShape3D" id="SphereShape3D_hitbox"] |
|
||||||
radius = 2.0 |
|
||||||
|
|
||||||
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_enemy"] |
|
||||||
properties/0/path = NodePath(".:position") |
|
||||||
properties/0/spawn = true |
|
||||||
properties/0/replication_mode = 1 |
|
||||||
properties/1/path = NodePath(".:rotation") |
|
||||||
properties/1/spawn = true |
|
||||||
properties/1/replication_mode = 1 |
|
||||||
|
|
||||||
[node name="BasicEnemy" type="CharacterBody3D"] |
|
||||||
collision_mask = 3 |
|
||||||
script = ExtResource("1_basic_enemy") |
|
||||||
move_speed = 3.5 |
|
||||||
attack_damage = 5.0 |
|
||||||
detection_range = 500.0 |
|
||||||
max_health = 20.0 |
|
||||||
|
|
||||||
[node name="Mesh" type="MeshInstance3D" parent="."] |
|
||||||
transform = Transform3D(-0.29981995, -0.01038597, -0.00033419454, 0.00636219, -0.19110087, 0.23117083, -0.008215994, 0.23102501, 0.19120643, 0, 0.41641736, 0) |
|
||||||
mesh = ExtResource("2_obxet") |
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."] |
|
||||||
transform = Transform3D(1.5, 0, 0, 0, 1.5, 0, 0, 0, 1.5, 0, 1, 0) |
|
||||||
shape = SubResource("CapsuleShape3D_body") |
|
||||||
|
|
||||||
[node name="HurtBox" type="Area3D" parent="." node_paths=PackedStringArray("owner_entity")] |
|
||||||
transform = Transform3D(1.5, 0, 0, 0, 1.5, 0, 0, 0, 1.5, 0, -0.47265148, 0) |
|
||||||
collision_layer = 16 |
|
||||||
collision_mask = 0 |
|
||||||
script = ExtResource("2_hurtbox") |
|
||||||
owner_entity = NodePath("..") |
|
||||||
|
|
||||||
[node name="HurtBoxShape" type="CollisionShape3D" parent="HurtBox"] |
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9349365, 0) |
|
||||||
shape = SubResource("CapsuleShape3D_body") |
|
||||||
|
|
||||||
[node name="HitBox" type="Area3D" parent="." node_paths=PackedStringArray("owner_entity")] |
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, -1.2) |
|
||||||
collision_layer = 0 |
|
||||||
collision_mask = 16 |
|
||||||
script = ExtResource("3_hitbox") |
|
||||||
damage = 15.0 |
|
||||||
knockback = 10.0 |
|
||||||
owner_entity = NodePath("..") |
|
||||||
|
|
||||||
[node name="HitBoxShape" type="CollisionShape3D" parent="HitBox"] |
|
||||||
transform = Transform3D(0.8, 0, 0, 0, 0.8, 0, 0, 0, 0.8, 0, 0.13992286, 0) |
|
||||||
shape = SubResource("SphereShape3D_hitbox") |
|
||||||
|
|
||||||
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."] |
|
||||||
replication_config = SubResource("SceneReplicationConfig_enemy") |
|
||||||
@ -1,51 +0,0 @@ |
|||||||
[gd_scene load_steps=7 format=3 uid="uid://dif4t1y3c07ax"] |
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://level/scripts/practice_dummy.gd" id="1_dummy"] |
|
||||||
[ext_resource type="Script" uid="uid://bj3uepduxvgju" path="res://level/scripts/hurt_box.gd" id="2_hurtbox"] |
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_dummy"] |
|
||||||
albedo_color = Color(0.8, 0.6, 0.4, 1) |
|
||||||
metallic = 0.2 |
|
||||||
roughness = 0.8 |
|
||||||
|
|
||||||
[sub_resource type="CapsuleMesh" id="CapsuleMesh_dummy"] |
|
||||||
material = SubResource("StandardMaterial3D_dummy") |
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"] |
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_hurtbox"] |
|
||||||
radius = 0.6 |
|
||||||
height = 2.2 |
|
||||||
|
|
||||||
[node name="PracticeDummy" type="CharacterBody3D"] |
|
||||||
transform = Transform3D(1.5, 0, 0, 0, 1.5, 0, 0, 0, 1.5, 0, 0, 0) |
|
||||||
collision_mask = 2 |
|
||||||
script = ExtResource("1_dummy") |
|
||||||
detection_range = 0.0 |
|
||||||
is_aggressive = false |
|
||||||
respawn_delay = 5.0 |
|
||||||
|
|
||||||
[node name="Mesh" type="MeshInstance3D" parent="."] |
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) |
|
||||||
mesh = SubResource("CapsuleMesh_dummy") |
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."] |
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) |
|
||||||
shape = SubResource("CapsuleShape3D_body") |
|
||||||
|
|
||||||
[node name="HurtBox" type="Area3D" parent="." node_paths=PackedStringArray("owner_entity")] |
|
||||||
collision_layer = 16 |
|
||||||
collision_mask = 0 |
|
||||||
script = ExtResource("2_hurtbox") |
|
||||||
owner_entity = NodePath("..") |
|
||||||
|
|
||||||
[node name="HurtBoxShape" type="CollisionShape3D" parent="HurtBox"] |
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) |
|
||||||
shape = SubResource("CapsuleShape3D_hurtbox") |
|
||||||
|
|
||||||
[node name="HealthLabel" type="Label3D" parent="."] |
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.5, 0) |
|
||||||
billboard = 1 |
|
||||||
text = "HP: 100/100" |
|
||||||
font_size = 24 |
|
||||||
outline_size = 8 |
|
||||||
@ -1,27 +0,0 @@ |
|||||||
[gd_scene load_steps=4 format=3 uid="uid://blm8lav3xh2yw"] |
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://level/scripts/enemy_spawner.gd" id="1_spawner"] |
|
||||||
[ext_resource type="PackedScene" uid="uid://byknup31d2b53" path="res://level/scenes/enemies/basic_enemy.tscn" id="2_basic_enemy"] |
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_indicator"] |
|
||||||
albedo_color = Color(1, 0.5, 0, 0.3) |
|
||||||
transparency = 1 |
|
||||||
cull_mode = 2 |
|
||||||
shading_mode = 0 |
|
||||||
|
|
||||||
[node name="EnemySpawner" type="Node3D"] |
|
||||||
script = ExtResource("1_spawner") |
|
||||||
spawn_radius = 20.0 |
|
||||||
spawn_height = 0.5 |
|
||||||
enemies_per_wave = 3 |
|
||||||
auto_start_next_wave = false |
|
||||||
wave_delay = 5.0 |
|
||||||
enemy_scenes = Array[PackedScene]([ExtResource("2_basic_enemy")]) |
|
||||||
|
|
||||||
[node name="SpawnRadiusIndicator" type="MeshInstance3D" parent="."] |
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, 0) |
|
||||||
visible = false |
|
||||||
material_override = SubResource("StandardMaterial3D_indicator") |
|
||||||
|
|
||||||
[node name="CenterMarker" type="MeshInstance3D" parent="."] |
|
||||||
transform = Transform3D(0.5, 0, 0, 0, 2, 0, 0, 0, 0.5, 0, 1, 0) |
|
||||||
@ -1,19 +0,0 @@ |
|||||||
[gd_scene load_steps=4 format=3 uid="uid://cehc5ckhq2byd"] |
|
||||||
|
|
||||||
[ext_resource type="PackedScene" uid="uid://c3e6e3s2q0uro" path="res://assets/Objects/Applecorer.glb" id="1_yadub"] |
|
||||||
[ext_resource type="Script" uid="uid://jyas86y3f0jp" path="res://level/scripts/hit_box.gd" id="2_lq7hu"] |
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="BoxShape3D_ei1hh"] |
|
||||||
size = Vector3(0.19293976, 2.9722443, 0.5605469) |
|
||||||
|
|
||||||
[node name="TestSwordMesh" type="Node3D"] |
|
||||||
|
|
||||||
[node name="Applecorer" parent="." instance=ExtResource("1_yadub")] |
|
||||||
transform = Transform3D(-0.3, 0, -2.6226834e-08, 0, 0.3, 0, 2.6226834e-08, 0, -0.3, 0, 0, 0) |
|
||||||
|
|
||||||
[node name="HitBox" type="Area3D" parent="."] |
|
||||||
script = ExtResource("2_lq7hu") |
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitBox"] |
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0034065247, 2.15522, 0.0234375) |
|
||||||
shape = SubResource("BoxShape3D_ei1hh") |
|
||||||
Binary file not shown.
@ -1,20 +0,0 @@ |
|||||||
[gd_scene load_steps=4 format=3 uid="uid://cq8r5mkn3wvxj"] |
|
||||||
|
|
||||||
[ext_resource type="PackedScene" uid="uid://cejg4ixtc5xsf" path="res://level/scenes/weapons/LobsterAxe.glb" id="1_lobster"] |
|
||||||
[ext_resource type="Script" uid="uid://jyas86y3f0jp" path="res://level/scripts/hit_box.gd" id="2_hitbox"] |
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="BoxShape3D_lobster"] |
|
||||||
size = Vector3(2, 3.2, 0.6) |
|
||||||
|
|
||||||
[node name="LobsterAxeMesh" type="Node3D"] |
|
||||||
|
|
||||||
[node name="LobsterAxe" parent="." instance=ExtResource("1_lobster")] |
|
||||||
transform = Transform3D(-1.3113416e-08, 0, 0.29999998, 0, 0.29999998, 0, -0.29999998, 0, -1.3113416e-08, 0, 0.72785115, 0) |
|
||||||
|
|
||||||
[node name="HitBox" type="Area3D" parent="."] |
|
||||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 0, 2.1950727, 0) |
|
||||||
script = ExtResource("2_hitbox") |
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitBox"] |
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.1840072, 0) |
|
||||||
shape = SubResource("BoxShape3D_lobster") |
|
||||||
@ -1,17 +0,0 @@ |
|||||||
[gd_scene load_steps=4 format=3 uid="uid://8c4l6s6x67vh"] |
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://ccnnd0y4jqiot" path="res://level/scripts/world_weapon.gd" id="1_7688s"] |
|
||||||
[ext_resource type="Resource" uid="uid://b2q62xc0jw4w3" path="res://level/resources/weapon_applecorer.tres" id="2_7688s"] |
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="1"] |
|
||||||
size = Vector3(0.3, 0.3, 1.2) |
|
||||||
|
|
||||||
[node name="WorldWeaponSword" type="RigidBody3D"] |
|
||||||
collision_layer = 4 |
|
||||||
collision_mask = 2 |
|
||||||
mass = 2.0 |
|
||||||
script = ExtResource("1_7688s") |
|
||||||
weapon_data = ExtResource("2_7688s") |
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."] |
|
||||||
shape = SubResource("1") |
|
||||||
@ -1,17 +0,0 @@ |
|||||||
[gd_scene load_steps=4 format=3 uid="uid://dpk7n3q8mwx2r"] |
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://ccnnd0y4jqiot" path="res://level/scripts/world_weapon.gd" id="1"] |
|
||||||
[ext_resource type="Resource" path="res://level/resources/weapon_lobsteraxe.tres" id="2"] |
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="1"] |
|
||||||
size = Vector3(0.4, 0.4, 0.8) |
|
||||||
|
|
||||||
[node name="WorldWeaponLobsterAxe" type="RigidBody3D"] |
|
||||||
collision_layer = 4 |
|
||||||
collision_mask = 2 |
|
||||||
mass = 2.5 |
|
||||||
script = ExtResource("1") |
|
||||||
weapon_data = ExtResource("2") |
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."] |
|
||||||
shape = SubResource("1") |
|
||||||
@ -1,728 +0,0 @@ |
|||||||
extends BaseEnemy |
|
||||||
class_name ArmedEnemy |
|
||||||
|
|
||||||
## An enemy that uses the player model, animations, and can equip weapons |
|
||||||
## Drops equipped weapons on death using the existing world weapon spawn system |
|
||||||
|
|
||||||
## Movement |
|
||||||
@export var move_speed: float = 4.0 |
|
||||||
@export var chase_range: float = 20.0 |
|
||||||
@export var attack_range: float = 2.5 |
|
||||||
|
|
||||||
## Combat (unarmed fallback) |
|
||||||
@export var unarmed_damage: float = 10.0 |
|
||||||
@export var unarmed_knockback: float = 5.0 |
|
||||||
@export var attack_cooldown: float = 1.0 |
|
||||||
@export_category("Unarmed Attack Timing") |
|
||||||
@export var unarmed_startup: float = 0.15 |
|
||||||
@export var unarmed_active: float = 0.2 |
|
||||||
|
|
||||||
## Weapon seeking (when unarmed) |
|
||||||
@export_category("Weapon Seeking") |
|
||||||
@export var seek_weapons_when_unarmed: bool = true ## If true, unarmed enemies will seek nearby weapons |
|
||||||
@export var weapon_seek_range: float = 30.0 ## How far to look for weapons |
|
||||||
@export var weapon_pickup_range: float = 1.5 ## How close to get before picking up |
|
||||||
|
|
||||||
## Weapon system |
|
||||||
@export_category("Weapons") |
|
||||||
@export var starting_weapon: WeaponData = null ## Weapon to equip on spawn |
|
||||||
@export var starting_offhand: WeaponData = null ## Off-hand weapon to equip on spawn |
|
||||||
|
|
||||||
## Body reference (LilguyBody for animations) |
|
||||||
@export var _body: Node3D = null |
|
||||||
@export var _weapon_attachment: BoneAttachment3D = null |
|
||||||
@export var _weapon_container: Node3D = null |
|
||||||
@export var _offhand_attachment: BoneAttachment3D = null |
|
||||||
@export var _offhand_container: Node3D = null |
|
||||||
|
|
||||||
## Runtime weapon state |
|
||||||
var equipped_weapon: BaseWeapon = null |
|
||||||
var equipped_offhand: BaseWeapon = null |
|
||||||
|
|
||||||
## AI State |
|
||||||
var _attack_timer: float = 0.0 |
|
||||||
var _is_attacking: bool = false |
|
||||||
var _unarmed_hitbox: HitBox = null |
|
||||||
|
|
||||||
## Visual feedback |
|
||||||
var _hit_flash_timer: float = 0.0 |
|
||||||
const HIT_FLASH_DURATION: float = 0.2 |
|
||||||
|
|
||||||
## Position sync (manual sync instead of MultiplayerSynchronizer for dynamic spawning) |
|
||||||
var _sync_timer: float = 0.0 |
|
||||||
const SYNC_INTERVAL: float = 0.05 # 20 times per second |
|
||||||
|
|
||||||
func _enter_tree(): |
|
||||||
# Enemies are always server-authoritative |
|
||||||
set_multiplayer_authority(1) |
|
||||||
|
|
||||||
func _ready(): |
|
||||||
super._ready() |
|
||||||
|
|
||||||
# Auto-find body if not set |
|
||||||
if _body == null: |
|
||||||
if has_node("LilguyRigged/Armature"): |
|
||||||
_body = get_node("LilguyRigged/Armature") |
|
||||||
|
|
||||||
# Auto-find weapon attachments |
|
||||||
if _weapon_attachment == null: |
|
||||||
_weapon_attachment = get_node_or_null("LilguyRigged/Armature/Skeleton3D/WeaponPoint") |
|
||||||
|
|
||||||
if _weapon_container == null and _weapon_attachment: |
|
||||||
_weapon_container = _weapon_attachment.get_node_or_null("WeaponContainer") |
|
||||||
|
|
||||||
if _offhand_attachment == null: |
|
||||||
_offhand_attachment = get_node_or_null("LilguyRigged/Armature/Skeleton3D/OffhandPoint") |
|
||||||
|
|
||||||
if _offhand_container == null and _offhand_attachment: |
|
||||||
_offhand_container = _offhand_attachment.get_node_or_null("OffhandContainer") |
|
||||||
|
|
||||||
# Setup unarmed hitbox |
|
||||||
call_deferred("_setup_unarmed_hitbox") |
|
||||||
|
|
||||||
# Equip starting weapons |
|
||||||
# Server will equip and send RPC to sync; clients also equip directly to handle late-join |
|
||||||
call_deferred("_equip_starting_weapons_local") |
|
||||||
|
|
||||||
## Equip starting weapons - server uses RPC to sync, clients equip directly |
|
||||||
func _equip_starting_weapons_local(): |
|
||||||
# Wait a frame to ensure everything is ready |
|
||||||
await get_tree().process_frame |
|
||||||
|
|
||||||
# Check if multiplayer peer is assigned |
|
||||||
if multiplayer.multiplayer_peer == null: |
|
||||||
# No multiplayer yet, just equip locally |
|
||||||
if starting_weapon: |
|
||||||
_equip_weapon(starting_weapon, false) |
|
||||||
if starting_offhand: |
|
||||||
_equip_weapon(starting_offhand, true) |
|
||||||
return |
|
||||||
|
|
||||||
if multiplayer.is_server(): |
|
||||||
# Server equips via RPC to sync to all clients |
|
||||||
if starting_weapon: |
|
||||||
print("[ArmedEnemy ", name, "] Server equipping starting weapon: ", starting_weapon.resource_path) |
|
||||||
rpc("_equip_weapon_sync", starting_weapon.resource_path, false) |
|
||||||
if starting_offhand: |
|
||||||
print("[ArmedEnemy ", name, "] Server equipping starting offhand: ", starting_offhand.resource_path) |
|
||||||
rpc("_equip_weapon_sync", starting_offhand.resource_path, true) |
|
||||||
else: |
|
||||||
# Client equips directly (for late-join clients who won't receive server's initial RPC) |
|
||||||
# Skip if already equipped (from server RPC) |
|
||||||
if starting_weapon and not equipped_weapon: |
|
||||||
print("[ArmedEnemy ", name, "] Client equipping starting weapon directly") |
|
||||||
_equip_weapon(starting_weapon, false) |
|
||||||
if starting_offhand and not equipped_offhand: |
|
||||||
print("[ArmedEnemy ", name, "] Client equipping starting offhand directly") |
|
||||||
_equip_weapon(starting_offhand, true) |
|
||||||
|
|
||||||
## Equip weapon on all clients |
|
||||||
@rpc("any_peer", "call_local", "reliable") |
|
||||||
func _equip_weapon_sync(weapon_data_path: String, is_offhand: bool): |
|
||||||
print("[ArmedEnemy ", name, "] _equip_weapon_sync called on peer ", multiplayer.get_unique_id(), " path: ", weapon_data_path) |
|
||||||
if weapon_data_path == "": |
|
||||||
push_error("[ArmedEnemy] Empty weapon path!") |
|
||||||
return |
|
||||||
var data = load(weapon_data_path) as WeaponData |
|
||||||
if data: |
|
||||||
_equip_weapon(data, is_offhand) |
|
||||||
else: |
|
||||||
push_error("[ArmedEnemy] Failed to load weapon data from: ", weapon_data_path) |
|
||||||
|
|
||||||
func _equip_weapon(data: WeaponData, is_offhand: bool = false): |
|
||||||
# Unequip current weapon in that hand first |
|
||||||
if is_offhand: |
|
||||||
if equipped_offhand: |
|
||||||
_unequip_weapon(true) |
|
||||||
else: |
|
||||||
if equipped_weapon: |
|
||||||
_unequip_weapon(false) |
|
||||||
|
|
||||||
# Determine attachment point |
|
||||||
var attach_point: Node3D |
|
||||||
if is_offhand: |
|
||||||
attach_point = _offhand_container if _offhand_container else _offhand_attachment |
|
||||||
else: |
|
||||||
attach_point = _weapon_container if _weapon_container else _weapon_attachment |
|
||||||
|
|
||||||
if not attach_point: |
|
||||||
push_error("[ArmedEnemy] No weapon attachment point found") |
|
||||||
return |
|
||||||
|
|
||||||
# Create weapon instance |
|
||||||
var weapon = BaseWeapon.new() |
|
||||||
weapon.weapon_data = data |
|
||||||
weapon.name = "EquippedOffHand" if is_offhand else "EquippedWeapon" |
|
||||||
|
|
||||||
# Add to scene first (so _ready is called and hitbox is set up) |
|
||||||
attach_point.add_child(weapon) |
|
||||||
|
|
||||||
# Set owner for damage routing (must be after add_child so hitbox exists) |
|
||||||
weapon.set_owner_character(self) |
|
||||||
|
|
||||||
# Store reference |
|
||||||
if is_offhand: |
|
||||||
equipped_offhand = weapon |
|
||||||
else: |
|
||||||
equipped_weapon = weapon |
|
||||||
|
|
||||||
print("[ArmedEnemy ", name, "] Equipped: ", data.weapon_name) |
|
||||||
|
|
||||||
func _unequip_weapon(is_offhand: bool = false): |
|
||||||
if is_offhand: |
|
||||||
if equipped_offhand: |
|
||||||
equipped_offhand.queue_free() |
|
||||||
equipped_offhand = null |
|
||||||
else: |
|
||||||
if equipped_weapon: |
|
||||||
equipped_weapon.queue_free() |
|
||||||
equipped_weapon = null |
|
||||||
|
|
||||||
func _process(delta): |
|
||||||
# Countdown timers |
|
||||||
if _attack_timer > 0: |
|
||||||
_attack_timer -= delta |
|
||||||
|
|
||||||
# Handle hit flash |
|
||||||
if _hit_flash_timer > 0: |
|
||||||
_hit_flash_timer -= delta |
|
||||||
if _hit_flash_timer <= 0: |
|
||||||
_reset_material() |
|
||||||
|
|
||||||
func _physics_process(delta): |
|
||||||
super._physics_process(delta) |
|
||||||
|
|
||||||
# Only server runs AI and movement (check peer is assigned first) |
|
||||||
if multiplayer.multiplayer_peer == null or not multiplayer.is_server(): |
|
||||||
return |
|
||||||
|
|
||||||
if is_dead: |
|
||||||
return |
|
||||||
|
|
||||||
# Apply gravity |
|
||||||
if not is_on_floor(): |
|
||||||
velocity.y -= ProjectSettings.get_setting("physics/3d/default_gravity") * delta |
|
||||||
|
|
||||||
# AI behavior - prioritize weapon seeking when unarmed |
|
||||||
var seeking_weapon = false |
|
||||||
if seek_weapons_when_unarmed and equipped_weapon == null: |
|
||||||
seeking_weapon = _ai_seek_weapon(delta) |
|
||||||
|
|
||||||
# If not seeking a weapon (or armed), do normal combat |
|
||||||
if not seeking_weapon: |
|
||||||
if current_target and is_instance_valid(current_target): |
|
||||||
_ai_combat(delta) |
|
||||||
else: |
|
||||||
# Stop moving if no target |
|
||||||
velocity.x = 0 |
|
||||||
velocity.z = 0 |
|
||||||
|
|
||||||
move_and_slide() |
|
||||||
|
|
||||||
# Rotate body to face movement direction (like player does) |
|
||||||
var body_rotation_y: float = 0.0 |
|
||||||
if _body and _body.has_method("apply_rotation") and velocity.length() > 0.1: |
|
||||||
_body.apply_rotation(velocity) |
|
||||||
if _body: |
|
||||||
body_rotation_y = _body.rotation.y |
|
||||||
|
|
||||||
# Animate body |
|
||||||
if _body and _body.has_method("animate"): |
|
||||||
_body.animate(velocity) |
|
||||||
|
|
||||||
# Sync position, rotation, and animation to clients periodically |
|
||||||
_sync_timer -= delta |
|
||||||
if _sync_timer <= 0: |
|
||||||
_sync_timer = SYNC_INTERVAL |
|
||||||
var current_anim = "" |
|
||||||
if _body: |
|
||||||
var anim_player = _body.get_node_or_null("../AnimationPlayer") as AnimationPlayer |
|
||||||
if anim_player: |
|
||||||
current_anim = anim_player.current_animation |
|
||||||
rpc("_sync_transform", global_position, body_rotation_y, current_anim) |
|
||||||
|
|
||||||
## Sync position, body rotation, and animation from server to clients |
|
||||||
@rpc("authority", "call_remote", "unreliable") |
|
||||||
func _sync_transform(pos: Vector3, body_rot_y: float, anim_name: String = ""): |
|
||||||
# Only apply on clients (server is authoritative) |
|
||||||
if multiplayer.is_server(): |
|
||||||
return |
|
||||||
|
|
||||||
global_position = pos |
|
||||||
if _body: |
|
||||||
_body.rotation.y = body_rot_y |
|
||||||
|
|
||||||
# Sync animation |
|
||||||
if anim_name != "": |
|
||||||
var anim_player = _body.get_node_or_null("../AnimationPlayer") as AnimationPlayer |
|
||||||
if anim_player and anim_player.has_animation(anim_name): |
|
||||||
if anim_player.current_animation != anim_name: |
|
||||||
anim_player.play(anim_name) |
|
||||||
|
|
||||||
## Override to find nearest player |
|
||||||
func get_nearest_player() -> Node: |
|
||||||
var players = get_players_in_range(1000.0) # Essentially unlimited |
|
||||||
|
|
||||||
if players.is_empty(): |
|
||||||
return null |
|
||||||
|
|
||||||
var nearest_player = null |
|
||||||
var nearest_distance = INF |
|
||||||
|
|
||||||
for player in players: |
|
||||||
var distance = global_position.distance_to(player.global_position) |
|
||||||
if distance < nearest_distance: |
|
||||||
nearest_distance = distance |
|
||||||
nearest_player = player |
|
||||||
|
|
||||||
return nearest_player |
|
||||||
|
|
||||||
## Find the nearest WorldWeapon within range |
|
||||||
func get_nearest_world_weapon() -> WorldWeapon: |
|
||||||
var level = get_tree().get_current_scene() |
|
||||||
if not level: |
|
||||||
return null |
|
||||||
|
|
||||||
var weapons_container = level.get_node_or_null("WeaponsContainer") |
|
||||||
if not weapons_container: |
|
||||||
return null |
|
||||||
|
|
||||||
var nearest_weapon: WorldWeapon = null |
|
||||||
var nearest_distance = weapon_seek_range |
|
||||||
|
|
||||||
for child in weapons_container.get_children(): |
|
||||||
if child is WorldWeapon: |
|
||||||
var distance = global_position.distance_to(child.global_position) |
|
||||||
if distance < nearest_distance: |
|
||||||
nearest_distance = distance |
|
||||||
nearest_weapon = child |
|
||||||
|
|
||||||
return nearest_weapon |
|
||||||
|
|
||||||
## Update target |
|
||||||
func _update_target(): |
|
||||||
if not is_aggressive: |
|
||||||
return |
|
||||||
|
|
||||||
var nearest = get_nearest_player() |
|
||||||
|
|
||||||
if nearest: |
|
||||||
if current_target != nearest: |
|
||||||
current_target = nearest |
|
||||||
target_changed.emit(nearest) |
|
||||||
else: |
|
||||||
if current_target != null: |
|
||||||
current_target = null |
|
||||||
target_changed.emit(null) |
|
||||||
|
|
||||||
## Combat AI |
|
||||||
func _ai_combat(delta): |
|
||||||
if not current_target or not is_instance_valid(current_target): |
|
||||||
return |
|
||||||
|
|
||||||
var target_pos = current_target.global_position |
|
||||||
var direction = (target_pos - global_position).normalized() |
|
||||||
var distance = global_position.distance_to(target_pos) |
|
||||||
|
|
||||||
# Get attack range - use a close fixed range to ensure hits connect |
|
||||||
# The hitbox is on the weapon in the enemy's hand, so we need to be close |
|
||||||
var current_attack_range = 2.0 # Fixed close range for melee |
|
||||||
|
|
||||||
# If in attack range, attack |
|
||||||
if distance <= current_attack_range: |
|
||||||
velocity.x = 0 |
|
||||||
velocity.z = 0 |
|
||||||
|
|
||||||
# Face target while attacking (use body rotation like player does) |
|
||||||
if _body and _body.has_method("apply_rotation"): |
|
||||||
var face_dir = Vector3(direction.x, 0, direction.z) * move_speed |
|
||||||
_body.apply_rotation(face_dir) |
|
||||||
|
|
||||||
if _attack_timer <= 0 and not _is_attacking: |
|
||||||
_perform_attack() |
|
||||||
else: |
|
||||||
# Chase target - velocity direction will be used for body rotation |
|
||||||
velocity.x = direction.x * move_speed |
|
||||||
velocity.z = direction.z * move_speed |
|
||||||
|
|
||||||
## Weapon seeking AI - returns true if actively seeking a weapon |
|
||||||
func _ai_seek_weapon(delta) -> bool: |
|
||||||
var nearest_weapon = get_nearest_world_weapon() |
|
||||||
if not nearest_weapon or not is_instance_valid(nearest_weapon): |
|
||||||
return false |
|
||||||
|
|
||||||
var weapon_pos = nearest_weapon.global_position |
|
||||||
var direction = (weapon_pos - global_position).normalized() |
|
||||||
var distance = global_position.distance_to(weapon_pos) |
|
||||||
|
|
||||||
# If close enough, pick up the weapon |
|
||||||
if distance <= weapon_pickup_range: |
|
||||||
velocity.x = 0 |
|
||||||
velocity.z = 0 |
|
||||||
_pickup_world_weapon(nearest_weapon) |
|
||||||
return true |
|
||||||
|
|
||||||
# Move towards the weapon |
|
||||||
velocity.x = direction.x * move_speed |
|
||||||
velocity.z = direction.z * move_speed |
|
||||||
return true |
|
||||||
|
|
||||||
## Pick up a world weapon (server only) |
|
||||||
func _pickup_world_weapon(world_weapon: WorldWeapon): |
|
||||||
if not multiplayer.is_server(): |
|
||||||
return |
|
||||||
|
|
||||||
if not world_weapon or not is_instance_valid(world_weapon): |
|
||||||
return |
|
||||||
|
|
||||||
var weapon_data = world_weapon.weapon_data |
|
||||||
if not weapon_data: |
|
||||||
return |
|
||||||
|
|
||||||
var resource_path = weapon_data.resource_path |
|
||||||
if resource_path == "": |
|
||||||
push_error("[ArmedEnemy] WorldWeapon has no resource path!") |
|
||||||
return |
|
||||||
|
|
||||||
var weapon_id = world_weapon.weapon_id |
|
||||||
if weapon_id == -1: |
|
||||||
push_error("[ArmedEnemy] WorldWeapon has invalid weapon_id!") |
|
||||||
return |
|
||||||
|
|
||||||
print("[ArmedEnemy ", name, "] Picking up weapon: ", weapon_data.weapon_name) |
|
||||||
|
|
||||||
# Equip the weapon on all clients |
|
||||||
rpc("_equip_weapon_sync", resource_path, false) |
|
||||||
|
|
||||||
# Remove the world weapon from all clients using level's system |
|
||||||
var level = get_tree().get_current_scene() |
|
||||||
if level and level.has_method("remove_world_weapon"): |
|
||||||
level.remove_world_weapon(weapon_id) |
|
||||||
else: |
|
||||||
push_error("[ArmedEnemy] Level doesn't have remove_world_weapon method!") |
|
||||||
|
|
||||||
## Perform attack |
|
||||||
func _perform_attack(): |
|
||||||
if _is_attacking or not multiplayer.is_server(): |
|
||||||
return |
|
||||||
|
|
||||||
# Use weapon if equipped |
|
||||||
if equipped_weapon and equipped_weapon.weapon_data: |
|
||||||
_perform_weapon_attack() |
|
||||||
else: |
|
||||||
_perform_unarmed_attack() |
|
||||||
|
|
||||||
func _perform_weapon_attack(): |
|
||||||
var weapon = equipped_weapon |
|
||||||
var data = weapon.weapon_data |
|
||||||
|
|
||||||
var total_duration = data.startup_time + data.active_time |
|
||||||
var cooldown = max(data.attack_cooldown, total_duration) |
|
||||||
|
|
||||||
_attack_timer = cooldown |
|
||||||
_is_attacking = true |
|
||||||
|
|
||||||
# Play animation on all clients |
|
||||||
var anim_name = data.attack_animation if data.attack_animation else "Attack_OneHand" |
|
||||||
rpc("_sync_attack_animation", anim_name) |
|
||||||
|
|
||||||
# Use weapon's built-in attack activation |
|
||||||
_activate_weapon_hitbox_direct(weapon) |
|
||||||
|
|
||||||
func _perform_unarmed_attack(): |
|
||||||
var total_duration = unarmed_startup + unarmed_active |
|
||||||
var cooldown = max(attack_cooldown, total_duration) |
|
||||||
|
|
||||||
_attack_timer = cooldown |
|
||||||
_is_attacking = true |
|
||||||
|
|
||||||
# Play animation |
|
||||||
rpc("_sync_attack_animation", "Attack_OneHand") |
|
||||||
|
|
||||||
# Activate unarmed hitbox |
|
||||||
_activate_unarmed_hitbox() |
|
||||||
|
|
||||||
## Activate weapon hitbox for attack (direct access to weapon's internal hitbox) |
|
||||||
func _activate_weapon_hitbox_direct(weapon: BaseWeapon): |
|
||||||
if not weapon or not multiplayer.is_server(): |
|
||||||
_is_attacking = false |
|
||||||
return |
|
||||||
|
|
||||||
var data = weapon.weapon_data |
|
||||||
|
|
||||||
# Access weapon's internal hitbox |
|
||||||
var hitbox = weapon._hitbox |
|
||||||
|
|
||||||
if not hitbox: |
|
||||||
print("[ArmedEnemy] No hitbox found on weapon, trying to find it") |
|
||||||
hitbox = weapon.get_node_or_null("HitBox") as HitBox |
|
||||||
|
|
||||||
if not hitbox: |
|
||||||
push_error("[ArmedEnemy] Cannot find hitbox on weapon!") |
|
||||||
_is_attacking = false |
|
||||||
return |
|
||||||
|
|
||||||
# Make sure hitbox has correct owner |
|
||||||
hitbox.owner_entity = self |
|
||||||
|
|
||||||
# STARTUP PHASE |
|
||||||
if data.startup_time > 0: |
|
||||||
await get_tree().create_timer(data.startup_time).timeout |
|
||||||
|
|
||||||
if not is_instance_valid(hitbox) or is_dead: |
|
||||||
_is_attacking = false |
|
||||||
return |
|
||||||
|
|
||||||
# ACTIVE PHASE |
|
||||||
hitbox.activate() |
|
||||||
|
|
||||||
await get_tree().create_timer(data.active_time).timeout |
|
||||||
|
|
||||||
# RECOVERY PHASE |
|
||||||
if hitbox and is_instance_valid(hitbox): |
|
||||||
hitbox.deactivate() |
|
||||||
|
|
||||||
_is_attacking = false |
|
||||||
|
|
||||||
## Setup unarmed hitbox |
|
||||||
func _setup_unarmed_hitbox(): |
|
||||||
_unarmed_hitbox = HitBox.new() |
|
||||||
_unarmed_hitbox.name = "UnarmedHitBox" |
|
||||||
_unarmed_hitbox.owner_entity = self |
|
||||||
_unarmed_hitbox.set_stats(unarmed_damage, unarmed_knockback) |
|
||||||
|
|
||||||
# Add collision shape BEFORE adding hitbox to tree (so _ready can find it) |
|
||||||
var collision = CollisionShape3D.new() |
|
||||||
var sphere = SphereShape3D.new() |
|
||||||
sphere.radius = attack_range |
|
||||||
collision.shape = sphere |
|
||||||
collision.position = Vector3(0, 0.8, -attack_range * 0.75) |
|
||||||
_unarmed_hitbox.add_child(collision) |
|
||||||
|
|
||||||
# Now attach the fully configured hitbox to body |
|
||||||
if _body: |
|
||||||
_body.add_child(_unarmed_hitbox) |
|
||||||
else: |
|
||||||
add_child(_unarmed_hitbox) |
|
||||||
|
|
||||||
# Connect hit signal |
|
||||||
_unarmed_hitbox.hit_landed.connect(_on_hitbox_hit) |
|
||||||
|
|
||||||
func _activate_unarmed_hitbox(): |
|
||||||
if not _unarmed_hitbox or not multiplayer.is_server(): |
|
||||||
_is_attacking = false |
|
||||||
return |
|
||||||
|
|
||||||
# STARTUP PHASE |
|
||||||
if unarmed_startup > 0: |
|
||||||
await get_tree().create_timer(unarmed_startup).timeout |
|
||||||
|
|
||||||
if not _unarmed_hitbox or not is_instance_valid(_unarmed_hitbox) or is_dead: |
|
||||||
_is_attacking = false |
|
||||||
return |
|
||||||
|
|
||||||
# ACTIVE PHASE |
|
||||||
_unarmed_hitbox.activate() |
|
||||||
|
|
||||||
await get_tree().create_timer(unarmed_active).timeout |
|
||||||
|
|
||||||
# RECOVERY PHASE |
|
||||||
if _unarmed_hitbox and is_instance_valid(_unarmed_hitbox): |
|
||||||
_unarmed_hitbox.deactivate() |
|
||||||
|
|
||||||
_is_attacking = false |
|
||||||
|
|
||||||
## Called when hitbox hits something |
|
||||||
func _on_hitbox_hit(target: Node, damage_amount: float, knockback_amount: float, attacker_pos: Vector3): |
|
||||||
if not target or not multiplayer.is_server(): |
|
||||||
return |
|
||||||
|
|
||||||
# Flash target's hurtbox |
|
||||||
if target is Node: |
|
||||||
var hurtbox = target.find_child("HurtBox", true, false) |
|
||||||
if hurtbox and hurtbox.has_method("flash_hit"): |
|
||||||
hurtbox.flash_hit() |
|
||||||
|
|
||||||
# Apply damage directly (we're server) |
|
||||||
if target is BaseUnit: |
|
||||||
target.take_damage(damage_amount, 1, knockback_amount, global_position) |
|
||||||
|
|
||||||
## Sync attack animation |
|
||||||
@rpc("any_peer", "call_local", "reliable") |
|
||||||
func _sync_attack_animation(anim_name: String): |
|
||||||
if _body and _body.has_method("play_attack"): |
|
||||||
_body.play_attack(anim_name) |
|
||||||
|
|
||||||
## Override hurt animation |
|
||||||
@rpc("any_peer", "call_local", "reliable") |
|
||||||
func _play_hurt_animation(): |
|
||||||
_flash_red() |
|
||||||
|
|
||||||
## Flash red when hit |
|
||||||
func _flash_red(): |
|
||||||
_hit_flash_timer = HIT_FLASH_DURATION |
|
||||||
|
|
||||||
# Flash all mesh instances in body |
|
||||||
if _body: |
|
||||||
var meshes = _find_mesh_instances(_body) |
|
||||||
for mesh in meshes: |
|
||||||
_apply_red_flash(mesh) |
|
||||||
|
|
||||||
func _find_mesh_instances(node: Node) -> Array[MeshInstance3D]: |
|
||||||
var meshes: Array[MeshInstance3D] = [] |
|
||||||
if node is MeshInstance3D: |
|
||||||
meshes.append(node) |
|
||||||
for child in node.get_children(): |
|
||||||
meshes.append_array(_find_mesh_instances(child)) |
|
||||||
return meshes |
|
||||||
|
|
||||||
func _apply_red_flash(mesh: MeshInstance3D): |
|
||||||
if not mesh: |
|
||||||
return |
|
||||||
|
|
||||||
for i in range(mesh.get_surface_override_material_count()): |
|
||||||
var material = mesh.get_surface_override_material(i) |
|
||||||
if not material: |
|
||||||
material = mesh.mesh.surface_get_material(i) |
|
||||||
if material: |
|
||||||
material = material.duplicate() |
|
||||||
mesh.set_surface_override_material(i, material) |
|
||||||
|
|
||||||
if material and material is StandardMaterial3D: |
|
||||||
material.albedo_color = Color(1.5, 0.3, 0.3) |
|
||||||
|
|
||||||
func _reset_material(): |
|
||||||
# Reset to a neutral color after flash |
|
||||||
if _body: |
|
||||||
var meshes = _find_mesh_instances(_body) |
|
||||||
for mesh in meshes: |
|
||||||
_reset_mesh_material(mesh) |
|
||||||
|
|
||||||
func _reset_mesh_material(mesh: MeshInstance3D): |
|
||||||
if not mesh: |
|
||||||
return |
|
||||||
|
|
||||||
for i in range(mesh.get_surface_override_material_count()): |
|
||||||
var material = mesh.get_surface_override_material(i) |
|
||||||
if material and material is StandardMaterial3D: |
|
||||||
# Reset to a default enemy color (reddish) |
|
||||||
material.albedo_color = Color(0.8, 0.3, 0.3) |
|
||||||
|
|
||||||
## Death callback - drop weapons |
|
||||||
func _on_enemy_died(killer_id: int): |
|
||||||
super._on_enemy_died(killer_id) |
|
||||||
|
|
||||||
# Hide body |
|
||||||
if _body: |
|
||||||
_body.visible = false |
|
||||||
|
|
||||||
# Disable collision so players can walk through |
|
||||||
var collision_shape = get_node_or_null("CollisionShape3D") |
|
||||||
if collision_shape: |
|
||||||
collision_shape.disabled = true |
|
||||||
|
|
||||||
# Disable hurtbox |
|
||||||
var hurtbox = get_node_or_null("HurtBox") |
|
||||||
if hurtbox: |
|
||||||
hurtbox.monitoring = false |
|
||||||
hurtbox.monitorable = false |
|
||||||
|
|
||||||
# Deactivate hitboxes |
|
||||||
if _unarmed_hitbox: |
|
||||||
_unarmed_hitbox.deactivate() |
|
||||||
|
|
||||||
# Drop equipped weapons (server only) |
|
||||||
if multiplayer.is_server(): |
|
||||||
_drop_all_weapons() |
|
||||||
|
|
||||||
print("[ArmedEnemy ", name, "] killed by ", killer_id) |
|
||||||
|
|
||||||
## Drop all equipped weapons |
|
||||||
func _drop_all_weapons(): |
|
||||||
if not multiplayer.is_server(): |
|
||||||
return |
|
||||||
|
|
||||||
# Drop main hand weapon |
|
||||||
if equipped_weapon and equipped_weapon.weapon_data: |
|
||||||
_spawn_dropped_weapon(equipped_weapon.weapon_data, false) |
|
||||||
|
|
||||||
# Drop off-hand weapon |
|
||||||
if equipped_offhand and equipped_offhand.weapon_data: |
|
||||||
_spawn_dropped_weapon(equipped_offhand.weapon_data, true) |
|
||||||
|
|
||||||
# Clear equipped weapons on all clients |
|
||||||
rpc("_clear_equipped_weapons") |
|
||||||
|
|
||||||
## Spawn a dropped weapon in the world |
|
||||||
func _spawn_dropped_weapon(data: WeaponData, is_offhand: bool): |
|
||||||
if not multiplayer.is_server(): |
|
||||||
return |
|
||||||
|
|
||||||
var resource_path = data.resource_path |
|
||||||
if resource_path == "": |
|
||||||
push_error("[ArmedEnemy] WeaponData has no resource path!") |
|
||||||
return |
|
||||||
|
|
||||||
# Calculate spawn position with slight offset and upward velocity |
|
||||||
var offset = Vector3.ZERO |
|
||||||
if is_offhand: |
|
||||||
offset = transform.basis.x * -0.5 # Left side |
|
||||||
else: |
|
||||||
offset = transform.basis.x * 0.5 # Right side |
|
||||||
|
|
||||||
var spawn_pos = global_position + offset |
|
||||||
spawn_pos.y += 1.5 # Spawn above death position |
|
||||||
|
|
||||||
# Random velocity to scatter weapons |
|
||||||
var velocity = Vector3( |
|
||||||
randf_range(-2.0, 2.0), |
|
||||||
randf_range(3.0, 5.0), # Upward |
|
||||||
randf_range(-2.0, 2.0) |
|
||||||
) |
|
||||||
|
|
||||||
# Use level's weapon spawning system |
|
||||||
var level = get_tree().get_current_scene() |
|
||||||
if level and level.has_method("spawn_world_weapon"): |
|
||||||
level._weapon_spawn_counter += 1 |
|
||||||
level.rpc("spawn_world_weapon", resource_path, spawn_pos, velocity, level._weapon_spawn_counter) |
|
||||||
print("[ArmedEnemy ", name, "] Dropped weapon: ", data.weapon_name) |
|
||||||
|
|
||||||
## Clear equipped weapons on all clients |
|
||||||
@rpc("any_peer", "call_local", "reliable") |
|
||||||
func _clear_equipped_weapons(): |
|
||||||
_unequip_weapon(false) |
|
||||||
_unequip_weapon(true) |
|
||||||
|
|
||||||
## Respawn callback |
|
||||||
func _on_enemy_respawned(): |
|
||||||
super._on_enemy_respawned() |
|
||||||
|
|
||||||
# Show body |
|
||||||
if _body: |
|
||||||
_body.visible = true |
|
||||||
_reset_material() |
|
||||||
|
|
||||||
# Re-enable collision |
|
||||||
var collision_shape = get_node_or_null("CollisionShape3D") |
|
||||||
if collision_shape: |
|
||||||
collision_shape.disabled = false |
|
||||||
|
|
||||||
# Re-enable hurtbox |
|
||||||
var hurtbox = get_node_or_null("HurtBox") |
|
||||||
if hurtbox: |
|
||||||
hurtbox.monitoring = false # Hurtbox doesn't monitor, it's monitored |
|
||||||
hurtbox.monitorable = true |
|
||||||
|
|
||||||
# Reset state |
|
||||||
_attack_timer = 0.0 |
|
||||||
_is_attacking = false |
|
||||||
|
|
||||||
# Re-equip starting weapons |
|
||||||
call_deferred("_equip_starting_weapons_local") |
|
||||||
|
|
||||||
print("[ArmedEnemy ", name, "] respawned") |
|
||||||
|
|
||||||
## Set enemy color (hue-based like player) |
|
||||||
func set_enemy_color(hue: float): |
|
||||||
if _body and _body.has_method("set_character_color"): |
|
||||||
_body.set_character_color(hue) |
|
||||||
@ -1 +0,0 @@ |
|||||||
uid://deefoag762nvc |
|
||||||
@ -1,108 +0,0 @@ |
|||||||
extends BaseUnit |
|
||||||
class_name BaseEnemy |
|
||||||
|
|
||||||
## Base class for all enemies in the game |
|
||||||
## Provides common enemy functionality like AI, pathfinding, and targeting |
|
||||||
|
|
||||||
signal target_changed(new_target: Node) |
|
||||||
|
|
||||||
## Current target (usually a player) |
|
||||||
var current_target: Node = null |
|
||||||
## Enemy detection/aggro range |
|
||||||
@export var detection_range: float = 10.0 |
|
||||||
## Whether this enemy is aggressive (will attack players) |
|
||||||
@export var is_aggressive: bool = true |
|
||||||
|
|
||||||
func _ready(): |
|
||||||
super._ready() |
|
||||||
|
|
||||||
# Enemies should respawn by default |
|
||||||
can_respawn = true |
|
||||||
|
|
||||||
# Connect to health signals for AI reactions |
|
||||||
health_changed.connect(_on_enemy_health_changed) |
|
||||||
died.connect(_on_enemy_died) |
|
||||||
respawned.connect(_on_enemy_respawned) |
|
||||||
|
|
||||||
func _physics_process(delta): |
|
||||||
# Only server handles enemy AI (check peer is assigned first) |
|
||||||
if multiplayer.multiplayer_peer == null or not multiplayer.is_server(): |
|
||||||
return |
|
||||||
|
|
||||||
if is_dead: |
|
||||||
return |
|
||||||
|
|
||||||
# Update target if needed |
|
||||||
_update_target() |
|
||||||
|
|
||||||
## Find and update current target |
|
||||||
func _update_target(): |
|
||||||
# Subclasses can override this to implement custom targeting logic |
|
||||||
pass |
|
||||||
|
|
||||||
## Get all players in range |
|
||||||
func get_players_in_range(range_dist: float) -> Array[Node]: |
|
||||||
var players_in_range: Array[Node] = [] |
|
||||||
|
|
||||||
# Find the players container |
|
||||||
var level = get_tree().get_current_scene() |
|
||||||
if not level or not level.has_node("PlayersContainer"): |
|
||||||
return players_in_range |
|
||||||
|
|
||||||
var players_container = level.get_node("PlayersContainer") |
|
||||||
|
|
||||||
for player in players_container.get_children(): |
|
||||||
if player is Character and not player.is_dead: |
|
||||||
var distance = global_position.distance_to(player.global_position) |
|
||||||
if distance <= range_dist: |
|
||||||
players_in_range.append(player) |
|
||||||
|
|
||||||
return players_in_range |
|
||||||
|
|
||||||
## Get nearest player |
|
||||||
func get_nearest_player() -> Node: |
|
||||||
var players = get_players_in_range(detection_range) |
|
||||||
|
|
||||||
if players.is_empty(): |
|
||||||
return null |
|
||||||
|
|
||||||
var nearest_player = null |
|
||||||
var nearest_distance = INF |
|
||||||
|
|
||||||
for player in players: |
|
||||||
var distance = global_position.distance_to(player.global_position) |
|
||||||
if distance < nearest_distance: |
|
||||||
nearest_distance = distance |
|
||||||
nearest_player = player |
|
||||||
|
|
||||||
return nearest_player |
|
||||||
|
|
||||||
## Health changed callback |
|
||||||
func _on_enemy_health_changed(old_health: float, new_health: float): |
|
||||||
# Subclasses can override to react to damage |
|
||||||
pass |
|
||||||
|
|
||||||
## Death callback |
|
||||||
func _on_enemy_died(killer_id: int): |
|
||||||
print("[Enemy ", name, "] died. Killer ID: ", killer_id) |
|
||||||
|
|
||||||
# Subclasses can override for death effects |
|
||||||
pass |
|
||||||
|
|
||||||
## Respawn callback |
|
||||||
func _on_enemy_respawned(): |
|
||||||
print("[Enemy ", name, "] respawned at ", global_position) |
|
||||||
|
|
||||||
# Clear target on respawn |
|
||||||
current_target = null |
|
||||||
target_changed.emit(null) |
|
||||||
|
|
||||||
# Subclasses can override for respawn effects |
|
||||||
pass |
|
||||||
|
|
||||||
## Override hurt animation |
|
||||||
@rpc("any_peer", "call_local", "reliable") |
|
||||||
func _play_hurt_animation(): |
|
||||||
# Flash red or play hurt animation |
|
||||||
# Subclasses should implement this with their specific animations |
|
||||||
pass |
|
||||||
@ -1 +0,0 @@ |
|||||||
uid://base_enemy_script |
|
||||||
@ -1,305 +0,0 @@ |
|||||||
extends BaseEnemy |
|
||||||
class_name BasicEnemy |
|
||||||
|
|
||||||
## A basic melee enemy that chases and attacks players |
|
||||||
## Server-authoritative AI with multiplayer support |
|
||||||
|
|
||||||
## Movement |
|
||||||
@export var move_speed: float = 3.0 |
|
||||||
@export var chase_range: float = 15.0 |
|
||||||
@export var attack_range: float = 2.5 |
|
||||||
|
|
||||||
## Combat |
|
||||||
@export var attack_damage: float = 15.0 |
|
||||||
@export var attack_knockback: float = 10.0 |
|
||||||
@export var attack_cooldown: float = 1.5 |
|
||||||
@export_category("Attack Timing") |
|
||||||
@export var attack_startup: float = 0.3 # Wind-up before hit |
|
||||||
@export var attack_active: float = 0.4 # Hit window duration |
|
||||||
|
|
||||||
## References |
|
||||||
var _hitbox: HitBox = null |
|
||||||
var _hurtbox: HurtBox = null |
|
||||||
var _mesh: MeshInstance3D = null |
|
||||||
|
|
||||||
## AI State |
|
||||||
var _attack_timer: float = 0.0 |
|
||||||
var _is_attacking: bool = false |
|
||||||
var _original_material: Material = null |
|
||||||
var _hit_flash_timer: float = 0.0 |
|
||||||
const HIT_FLASH_DURATION: float = 0.2 |
|
||||||
|
|
||||||
func _enter_tree(): |
|
||||||
# Enemies are always server-authoritative |
|
||||||
set_multiplayer_authority(1) |
|
||||||
|
|
||||||
func _ready(): |
|
||||||
super._ready() |
|
||||||
|
|
||||||
# Find mesh, hitbox, and hurtbox |
|
||||||
_mesh = get_node_or_null("Mesh") |
|
||||||
_hitbox = get_node_or_null("HitBox") |
|
||||||
_hurtbox = get_node_or_null("HurtBox") |
|
||||||
|
|
||||||
# Store original material for hit flash |
|
||||||
if _mesh: |
|
||||||
_original_material = _mesh.get_surface_override_material(0) |
|
||||||
if not _original_material and _mesh.mesh: |
|
||||||
_original_material = _mesh.mesh.surface_get_material(0) |
|
||||||
|
|
||||||
# Setup hitbox |
|
||||||
if _hitbox: |
|
||||||
_hitbox.owner_entity = self |
|
||||||
_hitbox.set_stats(attack_damage, attack_knockback) |
|
||||||
_hitbox.hit_landed.connect(_on_hitbox_hit) |
|
||||||
|
|
||||||
# Setup hurtbox |
|
||||||
if _hurtbox: |
|
||||||
_hurtbox.owner_entity = self |
|
||||||
|
|
||||||
func _process(delta): |
|
||||||
# Countdown timers |
|
||||||
if _attack_timer > 0: |
|
||||||
_attack_timer -= delta |
|
||||||
|
|
||||||
# Handle hit flash |
|
||||||
if _hit_flash_timer > 0: |
|
||||||
_hit_flash_timer -= delta |
|
||||||
if _hit_flash_timer <= 0: |
|
||||||
_reset_material() |
|
||||||
|
|
||||||
func _physics_process(delta): |
|
||||||
super._physics_process(delta) |
|
||||||
|
|
||||||
# Only server runs AI |
|
||||||
if not multiplayer.is_server(): |
|
||||||
return |
|
||||||
|
|
||||||
if is_dead: |
|
||||||
return |
|
||||||
|
|
||||||
# Apply gravity |
|
||||||
if not is_on_floor(): |
|
||||||
velocity.y -= ProjectSettings.get_setting("physics/3d/default_gravity") * delta |
|
||||||
|
|
||||||
# AI behavior |
|
||||||
if current_target and is_instance_valid(current_target): |
|
||||||
_ai_combat(delta) |
|
||||||
else: |
|
||||||
# Stop moving if no target |
|
||||||
velocity.x = 0 |
|
||||||
velocity.z = 0 |
|
||||||
|
|
||||||
move_and_slide() |
|
||||||
|
|
||||||
## Override to find nearest player without range limit |
|
||||||
func get_nearest_player() -> Node: |
|
||||||
# Find all players in a very large range (essentially unlimited) |
|
||||||
var players = get_players_in_range(1000.0) # 1000m range - basically unlimited |
|
||||||
|
|
||||||
if players.is_empty(): |
|
||||||
return null |
|
||||||
|
|
||||||
var nearest_player = null |
|
||||||
var nearest_distance = INF |
|
||||||
|
|
||||||
for player in players: |
|
||||||
var distance = global_position.distance_to(player.global_position) |
|
||||||
if distance < nearest_distance: |
|
||||||
nearest_distance = distance |
|
||||||
nearest_player = player |
|
||||||
|
|
||||||
return nearest_player |
|
||||||
|
|
||||||
## Update target - called by BaseEnemy |
|
||||||
func _update_target(): |
|
||||||
if not is_aggressive: |
|
||||||
return |
|
||||||
|
|
||||||
# Always find and target the nearest player (no range limit) |
|
||||||
var nearest = get_nearest_player() |
|
||||||
|
|
||||||
if nearest: |
|
||||||
# Update target if it changed |
|
||||||
if current_target != nearest: |
|
||||||
current_target = nearest |
|
||||||
target_changed.emit(nearest) |
|
||||||
else: |
|
||||||
# No players exist |
|
||||||
if current_target != null: |
|
||||||
current_target = null |
|
||||||
target_changed.emit(null) |
|
||||||
|
|
||||||
## Combat AI behavior |
|
||||||
func _ai_combat(delta): |
|
||||||
if not current_target or not is_instance_valid(current_target): |
|
||||||
return |
|
||||||
|
|
||||||
var target_pos = current_target.global_position |
|
||||||
var direction = (target_pos - global_position).normalized() |
|
||||||
var distance = global_position.distance_to(target_pos) |
|
||||||
|
|
||||||
# Face the target |
|
||||||
if direction.length() > 0.01: |
|
||||||
var look_dir = Vector3(direction.x, 0, direction.z) |
|
||||||
if look_dir.length() > 0.01: |
|
||||||
look_at(global_position + look_dir, Vector3.UP) |
|
||||||
|
|
||||||
# If in attack range, attack |
|
||||||
if distance <= attack_range: |
|
||||||
# Stop moving when attacking |
|
||||||
velocity.x = 0 |
|
||||||
velocity.z = 0 |
|
||||||
|
|
||||||
# Try to attack |
|
||||||
if _attack_timer <= 0 and not _is_attacking: |
|
||||||
_perform_attack() |
|
||||||
else: |
|
||||||
# Always chase the target (no range limit) |
|
||||||
velocity.x = direction.x * move_speed |
|
||||||
velocity.z = direction.z * move_speed |
|
||||||
|
|
||||||
## Perform melee attack |
|
||||||
func _perform_attack(): |
|
||||||
if _is_attacking or not multiplayer.is_server(): |
|
||||||
return |
|
||||||
|
|
||||||
# Calculate total attack duration |
|
||||||
var total_duration = attack_startup + attack_active |
|
||||||
var cooldown = max(attack_cooldown, total_duration) |
|
||||||
|
|
||||||
_attack_timer = cooldown |
|
||||||
_is_attacking = true |
|
||||||
|
|
||||||
# Play attack animation on all clients |
|
||||||
rpc("_sync_attack_animation") |
|
||||||
|
|
||||||
# Activate hitbox |
|
||||||
_activate_hitbox() |
|
||||||
|
|
||||||
## Activate hitbox for attack |
|
||||||
func _activate_hitbox(): |
|
||||||
if not _hitbox or not multiplayer.is_server(): |
|
||||||
_is_attacking = false |
|
||||||
return |
|
||||||
|
|
||||||
# STARTUP PHASE - Wait before activating |
|
||||||
if attack_startup > 0: |
|
||||||
await get_tree().create_timer(attack_startup).timeout |
|
||||||
|
|
||||||
if not _hitbox or not is_instance_valid(_hitbox) or is_dead: |
|
||||||
_is_attacking = false |
|
||||||
return |
|
||||||
|
|
||||||
# ACTIVE PHASE - Hitbox on |
|
||||||
_hitbox.activate() |
|
||||||
|
|
||||||
await get_tree().create_timer(attack_active).timeout |
|
||||||
|
|
||||||
# RECOVERY PHASE - Hitbox off |
|
||||||
if _hitbox and is_instance_valid(_hitbox): |
|
||||||
_hitbox.deactivate() |
|
||||||
|
|
||||||
_is_attacking = false |
|
||||||
|
|
||||||
## Called when hitbox hits something |
|
||||||
func _on_hitbox_hit(target: Node, damage_amount: float, knockback_amount: float, attacker_pos: Vector3): |
|
||||||
if not target or not multiplayer.is_server(): |
|
||||||
return |
|
||||||
|
|
||||||
# Flash target's hurtbox |
|
||||||
if target is Node: |
|
||||||
var hurtbox = target.find_child("HurtBox", true, false) |
|
||||||
if hurtbox and hurtbox.has_method("flash_hit"): |
|
||||||
hurtbox.flash_hit() |
|
||||||
|
|
||||||
# Server applies damage directly |
|
||||||
if target is BaseUnit: |
|
||||||
target.take_damage(damage_amount, 1, knockback_amount, global_position) |
|
||||||
|
|
||||||
## Play attack animation (synced to all clients) |
|
||||||
@rpc("any_peer", "call_local", "reliable") |
|
||||||
func _sync_attack_animation(): |
|
||||||
# Visual feedback for attack |
|
||||||
# Could play animation here if you have an AnimationPlayer |
|
||||||
pass |
|
||||||
|
|
||||||
## Override hurt animation to flash red |
|
||||||
@rpc("any_peer", "call_local", "reliable") |
|
||||||
func _play_hurt_animation(): |
|
||||||
if _mesh: |
|
||||||
_flash_red() |
|
||||||
|
|
||||||
## Flash red when hit |
|
||||||
func _flash_red(): |
|
||||||
if not _mesh: |
|
||||||
return |
|
||||||
|
|
||||||
_hit_flash_timer = HIT_FLASH_DURATION |
|
||||||
|
|
||||||
# Create red material |
|
||||||
var red_material = StandardMaterial3D.new() |
|
||||||
red_material.albedo_color = Color(1.5, 0.3, 0.3) |
|
||||||
|
|
||||||
# Copy properties from original if exists |
|
||||||
if _original_material and _original_material is StandardMaterial3D: |
|
||||||
var orig = _original_material as StandardMaterial3D |
|
||||||
red_material.metallic = orig.metallic |
|
||||||
red_material.roughness = orig.roughness |
|
||||||
red_material.albedo_texture = orig.albedo_texture |
|
||||||
|
|
||||||
_mesh.set_surface_override_material(0, red_material) |
|
||||||
|
|
||||||
## Reset material |
|
||||||
func _reset_material(): |
|
||||||
if _mesh and _original_material: |
|
||||||
_mesh.set_surface_override_material(0, _original_material.duplicate()) |
|
||||||
|
|
||||||
## Death callback |
|
||||||
func _on_enemy_died(killer_id: int): |
|
||||||
super._on_enemy_died(killer_id) |
|
||||||
|
|
||||||
# Hide when dead |
|
||||||
if _mesh: |
|
||||||
_mesh.visible = false |
|
||||||
|
|
||||||
# Disable collision so players can walk through |
|
||||||
var collision_shape = get_node_or_null("CollisionShape3D") |
|
||||||
if collision_shape: |
|
||||||
collision_shape.disabled = true |
|
||||||
|
|
||||||
# Disable hurtbox |
|
||||||
var hurtbox = get_node_or_null("HurtBox") |
|
||||||
if hurtbox: |
|
||||||
hurtbox.monitorable = false |
|
||||||
|
|
||||||
# Deactivate hitbox |
|
||||||
if _hitbox: |
|
||||||
_hitbox.deactivate() |
|
||||||
|
|
||||||
print("[BasicEnemy ", name, "] killed by ", killer_id) |
|
||||||
|
|
||||||
## Respawn callback |
|
||||||
func _on_enemy_respawned(): |
|
||||||
super._on_enemy_respawned() |
|
||||||
|
|
||||||
# Show mesh |
|
||||||
if _mesh: |
|
||||||
_mesh.visible = true |
|
||||||
_reset_material() |
|
||||||
|
|
||||||
# Re-enable collision |
|
||||||
var collision_shape = get_node_or_null("CollisionShape3D") |
|
||||||
if collision_shape: |
|
||||||
collision_shape.disabled = false |
|
||||||
|
|
||||||
# Re-enable hurtbox |
|
||||||
var hurtbox = get_node_or_null("HurtBox") |
|
||||||
if hurtbox: |
|
||||||
hurtbox.monitorable = true |
|
||||||
|
|
||||||
# Reset state |
|
||||||
_attack_timer = 0.0 |
|
||||||
_is_attacking = false |
|
||||||
|
|
||||||
print("[BasicEnemy ", name, "] respawned") |
|
||||||
@ -1 +0,0 @@ |
|||||||
uid://cd87rsuiqhdav |
|
||||||
@ -1,251 +0,0 @@ |
|||||||
extends Node3D |
|
||||||
class_name EnemySpawner |
|
||||||
|
|
||||||
## Spawns enemies in waves around the arena edge |
|
||||||
## Server-authoritative spawning with multiplayer sync |
|
||||||
|
|
||||||
signal wave_started(wave_number: int) |
|
||||||
signal wave_completed(wave_number: int) |
|
||||||
signal enemy_spawned(enemy: Node) |
|
||||||
signal all_enemies_defeated() |
|
||||||
|
|
||||||
## Spawn configuration |
|
||||||
@export_category("Spawn Settings") |
|
||||||
@export var spawn_radius: float = 20.0 ## Distance from spawner center to spawn enemies |
|
||||||
@export var spawn_height: float = 0.5 ## Height above ground to spawn |
|
||||||
@export var enemies_per_wave: int = 3 ## How many enemies spawn per wave |
|
||||||
@export var auto_start_next_wave: bool = false ## Auto-start next wave when all defeated |
|
||||||
@export var wave_delay: float = 5.0 ## Delay before next wave auto-starts |
|
||||||
|
|
||||||
@export_category("Enemy Pool") |
|
||||||
@export var enemy_scenes: Array[PackedScene] = [] ## List of enemy scenes to spawn from |
|
||||||
|
|
||||||
## Wave tracking |
|
||||||
var current_wave: int = 0 |
|
||||||
var active_enemies: Array[Node] = [] |
|
||||||
var _wave_delay_timer: float = 0.0 |
|
||||||
var _waiting_for_next_wave: bool = false |
|
||||||
var _enemy_id_counter: int = 0 |
|
||||||
|
|
||||||
## Debug visualization |
|
||||||
@export var show_spawn_radius: bool = false ## Show spawn radius circle in game |
|
||||||
var _debug_circle: MeshInstance3D = null |
|
||||||
|
|
||||||
func _ready(): |
|
||||||
print("[EnemySpawner] Ready. Server: ", multiplayer.is_server()) |
|
||||||
_create_debug_circle() |
|
||||||
|
|
||||||
## Start a new wave |
|
||||||
func start_wave(): |
|
||||||
if not multiplayer.is_server(): |
|
||||||
return |
|
||||||
|
|
||||||
if not is_inside_tree(): |
|
||||||
push_error("[EnemySpawner] Cannot start wave - spawner not in scene tree!") |
|
||||||
return |
|
||||||
|
|
||||||
if enemy_scenes.is_empty(): |
|
||||||
push_error("[EnemySpawner] No enemy scenes configured!") |
|
||||||
return |
|
||||||
|
|
||||||
current_wave += 1 |
|
||||||
print("[EnemySpawner] Starting wave ", current_wave) |
|
||||||
wave_started.emit(current_wave) |
|
||||||
|
|
||||||
# Spawn enemies |
|
||||||
for i in range(enemies_per_wave): |
|
||||||
_spawn_enemy(i, enemies_per_wave) |
|
||||||
|
|
||||||
## Spawn a single enemy at a position around the circle |
|
||||||
func _spawn_enemy(index: int, total: int): |
|
||||||
if not multiplayer.is_server(): |
|
||||||
return |
|
||||||
|
|
||||||
if not is_inside_tree(): |
|
||||||
push_error("[EnemySpawner] Cannot spawn enemy - spawner not in scene tree!") |
|
||||||
return |
|
||||||
|
|
||||||
if enemy_scenes.is_empty(): |
|
||||||
return |
|
||||||
|
|
||||||
# Pick a random enemy scene from the pool |
|
||||||
var enemy_scene = enemy_scenes[randi() % enemy_scenes.size()] |
|
||||||
|
|
||||||
# Calculate spawn position in a circle |
|
||||||
var angle = (TAU / total) * index # Evenly distribute around circle |
|
||||||
var offset = Vector3( |
|
||||||
cos(angle) * spawn_radius, |
|
||||||
spawn_height, |
|
||||||
sin(angle) * spawn_radius |
|
||||||
) |
|
||||||
var spawn_pos = global_position + offset |
|
||||||
|
|
||||||
# Generate unique name for multiplayer sync |
|
||||||
_enemy_id_counter += 1 |
|
||||||
var enemy_name = "Enemy_" + str(current_wave) + "_" + str(_enemy_id_counter) |
|
||||||
|
|
||||||
# Spawn on all clients via RPC (call_local will spawn on server too) |
|
||||||
var enemy_scene_path = enemy_scene.resource_path |
|
||||||
rpc("_spawn_enemy_on_client", enemy_name, spawn_pos, enemy_scene_path) |
|
||||||
|
|
||||||
## RPC to spawn enemy on all clients (including server via call_local) |
|
||||||
@rpc("any_peer", "call_local", "reliable") |
|
||||||
func _spawn_enemy_on_client(enemy_name: String, spawn_pos: Vector3, scene_path: String): |
|
||||||
# Load the enemy scene |
|
||||||
var enemy_scene = load(scene_path) |
|
||||||
if not enemy_scene: |
|
||||||
push_error("[EnemySpawner] Failed to load enemy scene: ", scene_path) |
|
||||||
return |
|
||||||
|
|
||||||
# Instantiate enemy |
|
||||||
var enemy = enemy_scene.instantiate() |
|
||||||
enemy.name = enemy_name |
|
||||||
enemy.position = spawn_pos |
|
||||||
|
|
||||||
# Find enemies container |
|
||||||
var level = get_tree().get_current_scene() |
|
||||||
var enemies_container = null |
|
||||||
if level and level.has_node("EnemiesContainer"): |
|
||||||
enemies_container = level.get_node("EnemiesContainer") |
|
||||||
else: |
|
||||||
push_error("[EnemySpawner] EnemiesContainer not found!") |
|
||||||
return |
|
||||||
|
|
||||||
# Add to scene |
|
||||||
enemies_container.add_child(enemy, true) |
|
||||||
|
|
||||||
# Only server tracks active enemies and connects signals |
|
||||||
if multiplayer.is_server(): |
|
||||||
active_enemies.append(enemy) |
|
||||||
|
|
||||||
# Connect death signal if enemy is BaseEnemy |
|
||||||
if enemy is BaseEnemy: |
|
||||||
enemy.died.connect(_on_enemy_died.bind(enemy)) |
|
||||||
|
|
||||||
enemy_spawned.emit(enemy) |
|
||||||
|
|
||||||
print("[EnemySpawner] Spawned ", enemy.name, " at ", enemy.global_position, " on peer ", multiplayer.get_unique_id()) |
|
||||||
|
|
||||||
## Called when an enemy dies |
|
||||||
func _on_enemy_died(killer_id: int, enemy: Node): |
|
||||||
if not multiplayer.is_server(): |
|
||||||
return |
|
||||||
|
|
||||||
print("[EnemySpawner] Enemy ", enemy.name, " defeated by ", killer_id) |
|
||||||
|
|
||||||
# Will be cleaned up in _update_active_enemies |
|
||||||
|
|
||||||
## Update list of active enemies and check if wave complete |
|
||||||
func _update_active_enemies(): |
|
||||||
if not multiplayer.is_server(): |
|
||||||
return |
|
||||||
|
|
||||||
# Remove dead/invalid enemies from active list |
|
||||||
var alive_count = 0 |
|
||||||
for enemy in active_enemies: |
|
||||||
if is_instance_valid(enemy) and enemy is BaseEnemy and not enemy.is_dead: |
|
||||||
alive_count += 1 |
|
||||||
|
|
||||||
# Check if wave is complete |
|
||||||
if active_enemies.size() > 0 and alive_count == 0: |
|
||||||
_on_wave_completed() |
|
||||||
|
|
||||||
## Called when all enemies in wave are defeated |
|
||||||
func _on_wave_completed(): |
|
||||||
if not multiplayer.is_server(): |
|
||||||
return |
|
||||||
|
|
||||||
print("[EnemySpawner] Wave ", current_wave, " completed!") |
|
||||||
wave_completed.emit(current_wave) |
|
||||||
|
|
||||||
# Clean up dead enemies after a delay |
|
||||||
await get_tree().create_timer(2.0).timeout |
|
||||||
_cleanup_dead_enemies() |
|
||||||
|
|
||||||
all_enemies_defeated.emit() |
|
||||||
|
|
||||||
# Auto-start next wave if enabled |
|
||||||
if auto_start_next_wave: |
|
||||||
_waiting_for_next_wave = true |
|
||||||
_wave_delay_timer = wave_delay |
|
||||||
print("[EnemySpawner] Next wave starts in ", wave_delay, " seconds") |
|
||||||
|
|
||||||
## Remove dead enemies from the scene |
|
||||||
func _cleanup_dead_enemies(): |
|
||||||
var cleaned = 0 |
|
||||||
for enemy in active_enemies: |
|
||||||
if is_instance_valid(enemy) and enemy is BaseEnemy and enemy.is_dead: |
|
||||||
enemy.queue_free() |
|
||||||
cleaned += 1 |
|
||||||
|
|
||||||
active_enemies.clear() |
|
||||||
print("[EnemySpawner] Cleaned up ", cleaned, " defeated enemies") |
|
||||||
|
|
||||||
## Manual wave control |
|
||||||
func start_next_wave(): |
|
||||||
if not multiplayer.is_server(): |
|
||||||
return |
|
||||||
start_wave() |
|
||||||
|
|
||||||
func stop_waves(): |
|
||||||
_waiting_for_next_wave = false |
|
||||||
|
|
||||||
## Get current wave info |
|
||||||
func get_alive_enemy_count() -> int: |
|
||||||
var count = 0 |
|
||||||
for enemy in active_enemies: |
|
||||||
if is_instance_valid(enemy) and enemy is BaseEnemy and not enemy.is_dead: |
|
||||||
count += 1 |
|
||||||
return count |
|
||||||
|
|
||||||
func is_wave_active() -> bool: |
|
||||||
return get_alive_enemy_count() > 0 |
|
||||||
|
|
||||||
## Create debug visualization circle |
|
||||||
func _create_debug_circle(): |
|
||||||
# Create a circle mesh to show spawn radius |
|
||||||
_debug_circle = MeshInstance3D.new() |
|
||||||
_debug_circle.name = "DebugSpawnCircle" |
|
||||||
|
|
||||||
# Create circle mesh using ImmediateMesh |
|
||||||
var immediate_mesh = ImmediateMesh.new() |
|
||||||
var material = StandardMaterial3D.new() |
|
||||||
material.albedo_color = Color(1, 0.5, 0, 0.6) # Orange |
|
||||||
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA |
|
||||||
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED |
|
||||||
material.cull_mode = BaseMaterial3D.CULL_DISABLED |
|
||||||
|
|
||||||
# Draw circle |
|
||||||
immediate_mesh.surface_begin(Mesh.PRIMITIVE_LINE_STRIP) |
|
||||||
var segments = 64 |
|
||||||
for i in range(segments + 1): |
|
||||||
var angle = (TAU / segments) * i |
|
||||||
var x = cos(angle) * spawn_radius |
|
||||||
var z = sin(angle) * spawn_radius |
|
||||||
immediate_mesh.surface_add_vertex(Vector3(x, spawn_height, z)) |
|
||||||
immediate_mesh.surface_end() |
|
||||||
|
|
||||||
_debug_circle.mesh = immediate_mesh |
|
||||||
_debug_circle.material_override = material |
|
||||||
_debug_circle.visible = show_spawn_radius |
|
||||||
add_child(_debug_circle) |
|
||||||
|
|
||||||
## Update debug visualization (useful when changing spawn_radius in editor) |
|
||||||
func _process(_delta): |
|
||||||
# Update circle visibility |
|
||||||
if _debug_circle: |
|
||||||
_debug_circle.visible = show_spawn_radius |
|
||||||
|
|
||||||
# Server spawning logic (check peer is assigned first) |
|
||||||
if multiplayer.multiplayer_peer == null or not multiplayer.is_server(): |
|
||||||
return |
|
||||||
|
|
||||||
# Handle wave delay timer |
|
||||||
if _waiting_for_next_wave: |
|
||||||
_wave_delay_timer -= _delta |
|
||||||
if _wave_delay_timer <= 0: |
|
||||||
_waiting_for_next_wave = false |
|
||||||
start_wave() |
|
||||||
|
|
||||||
# Check if all enemies defeated |
|
||||||
_update_active_enemies() |
|
||||||
@ -1 +0,0 @@ |
|||||||
uid://dim3dvik1fd27 |
|
||||||
@ -1,149 +0,0 @@ |
|||||||
extends Area3D |
|
||||||
class_name HitBox |
|
||||||
|
|
||||||
## A component that deals damage to HurtBoxes |
|
||||||
## Attach to weapons or attack effects |
|
||||||
## Uses direct physics queries for reliable hit detection |
|
||||||
|
|
||||||
signal hit_landed(target: Node, damage: float, knockback: float, attacker_pos: Vector3) |
|
||||||
|
|
||||||
## Damage dealt on hit |
|
||||||
@export var damage: float = 10.0 |
|
||||||
## Knockback force applied |
|
||||||
@export var knockback: float = 5.0 |
|
||||||
## Owner entity (used to prevent self-damage and identify attacker) |
|
||||||
@export var owner_entity: Node = null |
|
||||||
|
|
||||||
## Global debug visibility toggle (static-like via class name access) |
|
||||||
static var debug_visible: bool = true |
|
||||||
|
|
||||||
## Whether hitbox is currently active (only deals damage when active) |
|
||||||
var is_active: bool = false |
|
||||||
## Tracks entities hit this attack (prevents multi-hit) |
|
||||||
var _hits_this_attack: Array[Node] = [] |
|
||||||
## Shape for queries (extracted from child CollisionShape3D) |
|
||||||
var _query_shape: Shape3D = null |
|
||||||
## Debug mesh for visualization |
|
||||||
var _debug_mesh: MeshInstance3D = null |
|
||||||
var _debug_material: StandardMaterial3D = null |
|
||||||
|
|
||||||
func _ready(): |
|
||||||
# Find the collision shape for queries |
|
||||||
for child in get_children(): |
|
||||||
if child is CollisionShape3D and child.shape: |
|
||||||
_query_shape = child.shape |
|
||||||
_create_debug_visualization(child) |
|
||||||
break |
|
||||||
|
|
||||||
func _create_debug_visualization(collision_shape: CollisionShape3D): |
|
||||||
# Create a semi-transparent red mesh to visualize the hitbox |
|
||||||
_debug_mesh = MeshInstance3D.new() |
|
||||||
_debug_material = StandardMaterial3D.new() |
|
||||||
_debug_material.albedo_color = Color(1.0, 0.0, 0.0, 0.4) # Red, semi-transparent |
|
||||||
_debug_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA |
|
||||||
_debug_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED |
|
||||||
_debug_material.cull_mode = BaseMaterial3D.CULL_DISABLED # Visible from both sides |
|
||||||
|
|
||||||
# Create mesh matching the collision shape |
|
||||||
var mesh: Mesh = null |
|
||||||
if collision_shape.shape is BoxShape3D: |
|
||||||
var box_mesh = BoxMesh.new() |
|
||||||
box_mesh.size = collision_shape.shape.size |
|
||||||
mesh = box_mesh |
|
||||||
elif collision_shape.shape is SphereShape3D: |
|
||||||
var sphere_mesh = SphereMesh.new() |
|
||||||
sphere_mesh.radius = collision_shape.shape.radius |
|
||||||
sphere_mesh.height = collision_shape.shape.radius * 2 |
|
||||||
mesh = sphere_mesh |
|
||||||
elif collision_shape.shape is CapsuleShape3D: |
|
||||||
var capsule_mesh = CapsuleMesh.new() |
|
||||||
capsule_mesh.radius = collision_shape.shape.radius |
|
||||||
capsule_mesh.height = collision_shape.shape.height |
|
||||||
mesh = capsule_mesh |
|
||||||
|
|
||||||
if mesh: |
|
||||||
_debug_mesh.mesh = mesh |
|
||||||
_debug_mesh.material_override = _debug_material |
|
||||||
_debug_mesh.visible = HitBox.debug_visible |
|
||||||
# Don't set transform - it inherits from parent CollisionShape3D |
|
||||||
collision_shape.add_child(_debug_mesh) |
|
||||||
|
|
||||||
func _physics_process(_delta): |
|
||||||
# Update debug visibility |
|
||||||
if _debug_mesh: |
|
||||||
_debug_mesh.visible = HitBox.debug_visible |
|
||||||
|
|
||||||
if not is_active: |
|
||||||
return |
|
||||||
|
|
||||||
_check_hits() |
|
||||||
|
|
||||||
func _check_hits(): |
|
||||||
if not _query_shape: |
|
||||||
# Fallback: create a default sphere |
|
||||||
var sphere = SphereShape3D.new() |
|
||||||
sphere.radius = 2.0 |
|
||||||
_query_shape = sphere |
|
||||||
|
|
||||||
# Use physics server for reliable queries |
|
||||||
var space_state = get_world_3d().direct_space_state |
|
||||||
var query = PhysicsShapeQueryParameters3D.new() |
|
||||||
query.shape = _query_shape |
|
||||||
query.transform = global_transform |
|
||||||
query.collision_mask = 16 # Layer 5 (hurtbox) |
|
||||||
query.collide_with_areas = true |
|
||||||
query.collide_with_bodies = false |
|
||||||
|
|
||||||
var results = space_state.intersect_shape(query, 32) |
|
||||||
|
|
||||||
for result in results: |
|
||||||
var collider = result["collider"] |
|
||||||
if collider is HurtBox: |
|
||||||
_process_hit(collider) |
|
||||||
|
|
||||||
func _process_hit(hurtbox: HurtBox): |
|
||||||
# Don't hit our own hurtbox |
|
||||||
if hurtbox.owner_entity == owner_entity: |
|
||||||
return |
|
||||||
|
|
||||||
# Don't hit same entity twice in one attack |
|
||||||
if hurtbox.owner_entity in _hits_this_attack: |
|
||||||
return |
|
||||||
|
|
||||||
# Enemies don't damage other enemies (only players) |
|
||||||
if owner_entity is BaseEnemy and hurtbox.owner_entity is BaseEnemy: |
|
||||||
return |
|
||||||
|
|
||||||
# Register this hit |
|
||||||
var target = hurtbox.owner_entity |
|
||||||
if target: |
|
||||||
_hits_this_attack.append(target) |
|
||||||
|
|
||||||
# Get attacker position for knockback direction |
|
||||||
var attacker_pos = global_position |
|
||||||
if owner_entity and owner_entity is Node3D: |
|
||||||
attacker_pos = owner_entity.global_position |
|
||||||
|
|
||||||
# Emit signal - let the weapon/owner handle damage routing to server |
|
||||||
hit_landed.emit(target, damage, knockback, attacker_pos) |
|
||||||
|
|
||||||
## Activate hitbox (call when attack starts) |
|
||||||
func activate(): |
|
||||||
is_active = true |
|
||||||
_hits_this_attack.clear() |
|
||||||
# Change to yellow when active |
|
||||||
if _debug_material: |
|
||||||
_debug_material.albedo_color = Color(1.0, 1.0, 0.0, 0.5) # Yellow, semi-transparent |
|
||||||
|
|
||||||
## Deactivate hitbox (call when attack ends) |
|
||||||
func deactivate(): |
|
||||||
is_active = false |
|
||||||
_hits_this_attack.clear() |
|
||||||
# Change back to red when inactive |
|
||||||
if _debug_material: |
|
||||||
_debug_material.albedo_color = Color(1.0, 0.0, 0.0, 0.4) # Red, semi-transparent |
|
||||||
|
|
||||||
## Set damage stats (usually from weapon data) |
|
||||||
func set_stats(new_damage: float, new_knockback: float): |
|
||||||
damage = new_damage |
|
||||||
knockback = new_knockback |
|
||||||
@ -1 +0,0 @@ |
|||||||
uid://jyas86y3f0jp |
|
||||||
@ -1,94 +0,0 @@ |
|||||||
extends Area3D |
|
||||||
class_name HurtBox |
|
||||||
|
|
||||||
## A component that receives damage from HitBoxes |
|
||||||
## Attach to any entity that can be damaged (players, enemies, destructibles) |
|
||||||
## NOTE: This is a passive detection zone - HitBox handles the collision detection |
|
||||||
|
|
||||||
## Global debug visibility toggle (static-like via class name access) |
|
||||||
static var debug_visible: bool = true |
|
||||||
|
|
||||||
## The entity that owns this hurtbox (should be a BaseUnit or similar) |
|
||||||
@export var owner_entity: Node = null |
|
||||||
## Debug mesh for visualization |
|
||||||
var _debug_mesh: MeshInstance3D = null |
|
||||||
var _debug_material: StandardMaterial3D = null |
|
||||||
var _hit_flash_timer: float = 0.0 |
|
||||||
const HIT_FLASH_DURATION: float = 0.3 # seconds |
|
||||||
|
|
||||||
func _ready(): |
|
||||||
# Auto-find owner if not set (traverse up to find BaseUnit) |
|
||||||
if owner_entity == null: |
|
||||||
var parent = get_parent() |
|
||||||
while parent: |
|
||||||
if parent is BaseUnit: |
|
||||||
owner_entity = parent |
|
||||||
break |
|
||||||
parent = parent.get_parent() |
|
||||||
|
|
||||||
# Configure collision - hurtboxes are on layer 5, detect nothing (passive) |
|
||||||
collision_layer = 16 # Layer 5 (hurtbox) |
|
||||||
collision_mask = 0 # Don't detect anything - hitboxes detect us |
|
||||||
|
|
||||||
# Ensure we can be detected but don't detect others |
|
||||||
monitorable = true |
|
||||||
monitoring = false |
|
||||||
|
|
||||||
# Add debug visualization |
|
||||||
_create_debug_visualization() |
|
||||||
|
|
||||||
func _process(delta): |
|
||||||
# Update debug visibility |
|
||||||
if _debug_mesh: |
|
||||||
_debug_mesh.visible = HurtBox.debug_visible |
|
||||||
|
|
||||||
# Handle hit flash timer |
|
||||||
if _hit_flash_timer > 0.0: |
|
||||||
_hit_flash_timer -= delta |
|
||||||
if _hit_flash_timer <= 0.0: |
|
||||||
# Flash finished, return to green |
|
||||||
if _debug_material: |
|
||||||
_debug_material.albedo_color = Color(0.0, 1.0, 0.0, 0.3) # Green |
|
||||||
|
|
||||||
func _create_debug_visualization(): |
|
||||||
# Find the collision shape to visualize |
|
||||||
for child in get_children(): |
|
||||||
if child is CollisionShape3D and child.shape: |
|
||||||
# Create a semi-transparent green mesh to visualize the hurtbox |
|
||||||
_debug_mesh = MeshInstance3D.new() |
|
||||||
_debug_material = StandardMaterial3D.new() |
|
||||||
_debug_material.albedo_color = Color(0.0, 1.0, 0.0, 0.3) # Green, semi-transparent |
|
||||||
_debug_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA |
|
||||||
_debug_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED |
|
||||||
_debug_material.cull_mode = BaseMaterial3D.CULL_DISABLED # Visible from both sides |
|
||||||
|
|
||||||
# Create mesh matching the collision shape |
|
||||||
var mesh: Mesh = null |
|
||||||
if child.shape is BoxShape3D: |
|
||||||
var box_mesh = BoxMesh.new() |
|
||||||
box_mesh.size = child.shape.size |
|
||||||
mesh = box_mesh |
|
||||||
elif child.shape is SphereShape3D: |
|
||||||
var sphere_mesh = SphereMesh.new() |
|
||||||
sphere_mesh.radius = child.shape.radius |
|
||||||
sphere_mesh.height = child.shape.radius * 2 |
|
||||||
mesh = sphere_mesh |
|
||||||
elif child.shape is CapsuleShape3D: |
|
||||||
var capsule_mesh = CapsuleMesh.new() |
|
||||||
capsule_mesh.radius = child.shape.radius |
|
||||||
capsule_mesh.height = child.shape.height |
|
||||||
mesh = capsule_mesh |
|
||||||
|
|
||||||
if mesh: |
|
||||||
_debug_mesh.mesh = mesh |
|
||||||
_debug_mesh.material_override = _debug_material |
|
||||||
_debug_mesh.visible = HurtBox.debug_visible |
|
||||||
# Don't set transform - it inherits from parent CollisionShape3D |
|
||||||
child.add_child(_debug_mesh) |
|
||||||
break |
|
||||||
|
|
||||||
## Call this when the hurtbox is hit to flash red |
|
||||||
func flash_hit(): |
|
||||||
_hit_flash_timer = HIT_FLASH_DURATION |
|
||||||
if _debug_material: |
|
||||||
_debug_material.albedo_color = Color(1.0, 0.0, 0.0, 0.5) # Red, semi-transparent |
|
||||||
@ -1 +0,0 @@ |
|||||||
uid://bj3uepduxvgju |
|
||||||
@ -1,132 +0,0 @@ |
|||||||
extends BaseEnemy |
|
||||||
class_name PracticeDummy |
|
||||||
|
|
||||||
## A stationary practice dummy for testing combat |
|
||||||
## Cannot move or attack - just takes damage and shows health |
|
||||||
|
|
||||||
## Visual mesh reference |
|
||||||
@onready var _mesh: MeshInstance3D = null |
|
||||||
## Health label above dummy |
|
||||||
@onready var _health_label: Label3D = null |
|
||||||
## Original material for hit flash effect |
|
||||||
var _original_material: Material = null |
|
||||||
## Hit flash effect |
|
||||||
var _hit_flash_timer: float = 0.0 |
|
||||||
const HIT_FLASH_DURATION: float = 0.2 |
|
||||||
|
|
||||||
func _ready(): |
|
||||||
super._ready() |
|
||||||
|
|
||||||
# Practice dummy should not be aggressive |
|
||||||
is_aggressive = false |
|
||||||
|
|
||||||
# Auto-find mesh and health label |
|
||||||
_mesh = get_node_or_null("Mesh") |
|
||||||
_health_label = get_node_or_null("HealthLabel") |
|
||||||
|
|
||||||
# Store original material for flash effect |
|
||||||
if _mesh: |
|
||||||
_original_material = _mesh.get_surface_override_material(0) |
|
||||||
if not _original_material and _mesh.mesh: |
|
||||||
_original_material = _mesh.mesh.surface_get_material(0) |
|
||||||
|
|
||||||
# Update initial health display |
|
||||||
_update_health_display() |
|
||||||
|
|
||||||
# Connect health change to update display |
|
||||||
health_changed.connect(_on_health_display_changed) |
|
||||||
|
|
||||||
func _process(delta): |
|
||||||
# Handle hit flash timer |
|
||||||
if _hit_flash_timer > 0: |
|
||||||
_hit_flash_timer -= delta |
|
||||||
if _hit_flash_timer <= 0: |
|
||||||
_reset_material() |
|
||||||
|
|
||||||
func _physics_process(delta): |
|
||||||
# Don't call super._physics_process since we don't move |
|
||||||
# Just apply gravity |
|
||||||
if not is_on_floor(): |
|
||||||
velocity.y -= ProjectSettings.get_setting("physics/3d/default_gravity") * delta |
|
||||||
move_and_slide() |
|
||||||
|
|
||||||
## Update health display |
|
||||||
func _update_health_display(): |
|
||||||
if _health_label: |
|
||||||
_health_label.text = "HP: %d/%d" % [int(current_health), int(max_health)] |
|
||||||
|
|
||||||
func _on_health_display_changed(_old_health: float, _new_health: float): |
|
||||||
_update_health_display() |
|
||||||
|
|
||||||
## Override hurt animation to flash red |
|
||||||
@rpc("any_peer", "call_local", "reliable") |
|
||||||
func _play_hurt_animation(): |
|
||||||
if _mesh: |
|
||||||
_flash_red() |
|
||||||
|
|
||||||
## Flash the dummy red when hit |
|
||||||
func _flash_red(): |
|
||||||
if not _mesh: |
|
||||||
return |
|
||||||
|
|
||||||
_hit_flash_timer = HIT_FLASH_DURATION |
|
||||||
|
|
||||||
# Create red material |
|
||||||
var red_material = StandardMaterial3D.new() |
|
||||||
red_material.albedo_color = Color(1.5, 0.3, 0.3) # Bright red |
|
||||||
|
|
||||||
# Copy properties from original if it exists |
|
||||||
if _original_material and _original_material is StandardMaterial3D: |
|
||||||
var orig = _original_material as StandardMaterial3D |
|
||||||
red_material.metallic = orig.metallic |
|
||||||
red_material.roughness = orig.roughness |
|
||||||
red_material.albedo_texture = orig.albedo_texture |
|
||||||
|
|
||||||
_mesh.set_surface_override_material(0, red_material) |
|
||||||
|
|
||||||
## Reset to original material |
|
||||||
func _reset_material(): |
|
||||||
if _mesh and _original_material: |
|
||||||
_mesh.set_surface_override_material(0, _original_material.duplicate()) |
|
||||||
|
|
||||||
## Override death to just hide the mesh |
|
||||||
func _on_enemy_died(killer_id: int): |
|
||||||
super._on_enemy_died(killer_id) |
|
||||||
|
|
||||||
# Hide mesh when dead |
|
||||||
if _mesh: |
|
||||||
_mesh.visible = false |
|
||||||
|
|
||||||
# Disable collision so players can walk through |
|
||||||
var collision_shape = get_node_or_null("CollisionShape3D") |
|
||||||
if collision_shape: |
|
||||||
collision_shape.disabled = true |
|
||||||
|
|
||||||
# Disable hurtbox |
|
||||||
var hurtbox = get_node_or_null("HurtBox") |
|
||||||
if hurtbox: |
|
||||||
hurtbox.monitorable = false |
|
||||||
|
|
||||||
print("[PracticeDummy] Killed by player ", killer_id, ". Respawning in ", respawn_delay, " seconds...") |
|
||||||
|
|
||||||
## Override respawn to show mesh again |
|
||||||
func _on_enemy_respawned(): |
|
||||||
super._on_enemy_respawned() |
|
||||||
|
|
||||||
# Show mesh when respawned |
|
||||||
if _mesh: |
|
||||||
_mesh.visible = true |
|
||||||
_reset_material() |
|
||||||
|
|
||||||
# Re-enable collision |
|
||||||
var collision_shape = get_node_or_null("CollisionShape3D") |
|
||||||
if collision_shape: |
|
||||||
collision_shape.disabled = false |
|
||||||
|
|
||||||
# Re-enable hurtbox |
|
||||||
var hurtbox = get_node_or_null("HurtBox") |
|
||||||
if hurtbox: |
|
||||||
hurtbox.monitorable = true |
|
||||||
|
|
||||||
_update_health_display() |
|
||||||
print("[PracticeDummy] Respawned!") |
|
||||||
@ -1 +0,0 @@ |
|||||||
uid://practice_dummy_script |
|
||||||
Loading…
Reference in new issue