Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b606563f4d | ||
|
|
fab47fb1e0 | ||
|
|
ddc1522174 | ||
|
|
7d18de1621 | ||
|
|
10cc8720b7 | ||
|
|
2ec7d56511 | ||
|
|
b1f1016475 | ||
|
|
e192802f01 | ||
|
|
85e76d67ca | ||
|
|
6e4e3a3585 | ||
|
|
bdc507e852 |
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
[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
|
||||
attack_animation = "Attack_TwoHandSwing"
|
||||
knockback_force = 12.0
|
||||
startup_time = 0.2
|
||||
active_time = 0.15
|
||||
mesh_scene = ExtResource("1_1ytxi")
|
||||
weight = 2.0
|
||||
@@ -12,6 +12,8 @@ damage = 8.0
|
||||
attack_range = 2.5
|
||||
attack_cooldown = 0.8
|
||||
knockback_force = 15.0
|
||||
startup_time = 0.25
|
||||
active_time = 0.15
|
||||
can_block = true
|
||||
block_reduction = 0.7
|
||||
mesh_scene = ExtResource("2")
|
||||
|
||||
@@ -12,6 +12,8 @@ attack_range = 3.5
|
||||
attack_cooldown = 0.6
|
||||
knockback_force = 12.0
|
||||
attack_animation = "Attack1"
|
||||
startup_time = 0.12
|
||||
active_time = 0.2
|
||||
mesh_scene = ExtResource("2")
|
||||
pickup_radius = 1.5
|
||||
weight = 2.0
|
||||
|
||||
@@ -11,5 +11,7 @@ damage = 20.0
|
||||
attack_range = 3.5
|
||||
attack_cooldown = 0.6
|
||||
knockback_force = 12.0
|
||||
startup_time = 0.1
|
||||
active_time = 1.0
|
||||
mesh_scene = ExtResource("1_gdc1w")
|
||||
weight = 2.0
|
||||
|
||||
@@ -1,122 +1,152 @@
|
||||
[gd_scene load_steps=6 format=3 uid="uid://db06e8q8f8bdq"]
|
||||
[gd_scene load_steps=9 format=3 uid="uid://db06e8q8f8bdq"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://byw3ig2bs1wgu" path="res://assets/characters/player/LilguyRigged.glb" id="1_e6qwr"]
|
||||
[ext_resource type="Script" uid="uid://c2si8gkbnde0c" path="res://level/scripts/player.gd" id="1_player"]
|
||||
[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://bj7yrijm7bppq" path="res://level/scripts/spring_arm_offset.gd" id="9_dlyie"]
|
||||
[ext_resource type="Script" uid="uid://bj7yrijm7bppq" path="res://level/scripts/spring_arm_offset.gd" id="4_spring"]
|
||||
[ext_resource type="Script" uid="uid://bj3uepduxvgju" path="res://level/scripts/hurt_box.gd" id="5_hurtbox"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_yxyay"]
|
||||
radius = 0.35796
|
||||
height = 1.73092
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_hurtbox"]
|
||||
radius = 0.4
|
||||
height = 1.8
|
||||
|
||||
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_xbohm"]
|
||||
properties/0/path = NodePath(".:position")
|
||||
properties/0/spawn = true
|
||||
properties/0/replication_mode = 1
|
||||
properties/1/path = NodePath("AnimationPlayer:current_animation")
|
||||
properties/1/path = NodePath("LilguyRigged/AnimationPlayer:current_animation")
|
||||
properties/1/spawn = true
|
||||
properties/1/replication_mode = 1
|
||||
properties/2/path = NodePath("PlayerNick/Nickname:text")
|
||||
properties/2/spawn = true
|
||||
properties/2/replication_mode = 1
|
||||
properties/3/path = NodePath("Armature:rotation")
|
||||
properties/3/path = NodePath("LilguyRigged/Armature:rotation")
|
||||
properties/3/spawn = true
|
||||
properties/3/replication_mode = 1
|
||||
|
||||
[node name="LilguyRigged" instance=ExtResource("1_e6qwr")]
|
||||
[node name="Player" type="CharacterBody3D" node_paths=PackedStringArray("_body", "_spring_arm_offset", "_weapon_attachment", "_weapon_container", "_offhand_attachment", "_offhand_container")]
|
||||
collision_mask = 3
|
||||
script = ExtResource("1_player")
|
||||
_body = NodePath("LilguyRigged/Armature")
|
||||
_spring_arm_offset = NodePath("SpringArmOffset")
|
||||
_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")
|
||||
|
||||
[node name="Armature" parent="." index="0" node_paths=PackedStringArray("_character", "animation_player")]
|
||||
[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("..")
|
||||
_character = NodePath("../..")
|
||||
animation_player = NodePath("../AnimationPlayer")
|
||||
|
||||
[node name="Skeleton3D" parent="Armature" index="0"]
|
||||
bones/0/position = Vector3(-0.32852697, 2.914154, -546.76843)
|
||||
bones/0/rotation = Quaternion(-0.6608288, 0.28933647, -0.19178489, 0.6654384)
|
||||
[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.015321622, 0.025352472, 0.09471857, 0.99506325)
|
||||
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.03465983, 0.05047194, 0.051301006, 0.99680465)
|
||||
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.029244617, 0.05379084, -0.051849358, 0.9967763)
|
||||
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.0006216668, 0.08164933, 0.020402616, 0.99645215)
|
||||
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.03735582, 0.19943666, -0.04159315, 0.97831464)
|
||||
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.8036073, -0.09628729, 0.106725484, 0.57754105)
|
||||
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.25522032, -0.08967137, 0.029357875, 0.962268)
|
||||
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.0878422, -0.16096674, 0.2433837, 0.9524378)
|
||||
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.14271575, -0.5852634, 0.782287, 0.15851216)
|
||||
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.32196987, 0.13412467, 0.27112576, 0.8971269)
|
||||
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.09037647, 0.101556264, -0.39819276, 0.90717196)
|
||||
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.38543195, 0.163806, 0.8217493, 0.3864424)
|
||||
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.05300442, 0.17209676, 0.3905684, 0.90278995)
|
||||
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.24982396, 0.64725155, -0.6711768, 0.2611037)
|
||||
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.11539763, 0.017187234, -0.0100022685, 0.9931203)
|
||||
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.07370265, -0.18747172, 0.9412239, 0.2711456)
|
||||
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.03735361, -0.04220169, 0.46135134, 0.8854257)
|
||||
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.7933202, 0.12858748, -0.3622723, 0.47208822)
|
||||
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.009521758, -0.0077993367, 0.9928351)
|
||||
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="Armature/Skeleton3D" index="1"]
|
||||
transform = Transform3D(-0.43292555, -0.61284775, 0.6610542, 0.7782953, 0.11585887, 0.61711675, -0.45478758, 0.78166103, 0.42681834, -352.38528, -73.5694, -531.96124)
|
||||
[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="Armature/Skeleton3D/WeaponPoint" index="0"]
|
||||
[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="Armature/Skeleton3D" index="2"]
|
||||
transform = Transform3D(0.6212382, -0.004605584, -0.7836083, -0.620316, 0.60813713, -0.49535576, 0.47882265, 0.7938187, 0.37494114, 135.65903, 334.35764, -511.2708)
|
||||
[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="Armature/Skeleton3D/OffhandPoint" index="0"]
|
||||
[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="CollisionShape3D" type="CollisionShape3D" parent="." index="2"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.066, 0.828, 0.01)
|
||||
[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_yxyay")
|
||||
|
||||
[node name="SpringArmOffset" type="Node3D" parent="." index="3" node_paths=PackedStringArray("_spring_arm")]
|
||||
[node name="HurtBox" type="Area3D" parent="." node_paths=PackedStringArray("owner_entity")]
|
||||
collision_layer = 16
|
||||
collision_mask = 0
|
||||
script = ExtResource("5_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="SpringArmOffset" type="Node3D" parent="." node_paths=PackedStringArray("_spring_arm")]
|
||||
transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 0)
|
||||
script = ExtResource("9_dlyie")
|
||||
script = ExtResource("4_spring")
|
||||
_spring_arm = NodePath("SpringArm3D")
|
||||
|
||||
[node name="SpringArm3D" type="SpringArm3D" parent="SpringArmOffset" index="0"]
|
||||
[node name="SpringArm3D" type="SpringArm3D" parent="SpringArmOffset"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
|
||||
spring_length = 5.0
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="SpringArmOffset/SpringArm3D" index="0"]
|
||||
[node name="Camera3D" type="Camera3D" parent="SpringArmOffset/SpringArm3D"]
|
||||
current = true
|
||||
|
||||
[node name="PlayerNick" type="Node3D" parent="." index="4"]
|
||||
[node name="PlayerNick" type="Node3D" parent="."]
|
||||
|
||||
[node name="Nickname" type="Label3D" parent="PlayerNick" index="0"]
|
||||
[node name="Nickname" type="Label3D" parent="PlayerNick"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.97037, 0)
|
||||
billboard = 1
|
||||
outline_modulate = Color(0, 0, 0, 0.301961)
|
||||
text = "player name test"
|
||||
|
||||
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="." index="5"]
|
||||
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
|
||||
replication_config = SubResource("SceneReplicationConfig_xbohm")
|
||||
|
||||
[editable path="LilguyRigged"]
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
[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
|
||||
+11
-3
@@ -1,10 +1,12 @@
|
||||
[gd_scene load_steps=19 format=3 uid="uid://dugaivbj1o66n"]
|
||||
[gd_scene load_steps=21 format=3 uid="uid://dugaivbj1o66n"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://d0dgljwwl463n" path="res://level/scripts/level.gd" id="1_e1sh7"]
|
||||
[ext_resource type="PackedScene" uid="uid://db06e8q8f8bdq" path="res://level/scenes/Player_Lilguy.tscn" id="1_uvcbi"]
|
||||
[ext_resource type="FontFile" uid="uid://diapabmalpcrj" path="res://assets/fonts/Kurland.ttf" id="3_icc4p"]
|
||||
[ext_resource type="PackedScene" uid="uid://b48oxbcgxu3d8" path="res://assets/Objects/Colosseum_10.fbx" id="4_u750a"]
|
||||
[ext_resource type="PackedScene" path="res://level/scenes/enemies/practice_dummy.tscn" id="3_i7s07"]
|
||||
[ext_resource type="FontFile" uid="uid://wipqjhfqeuwd" path="res://assets/fonts/Kurland.ttf" id="3_icc4p"]
|
||||
[ext_resource type="PackedScene" uid="uid://chkrcwlprbn88" path="res://assets/Objects/Colosseum_10.fbx" id="4_u750a"]
|
||||
[ext_resource type="PackedScene" uid="uid://hd6pq287rgye" path="res://level/scenes/weapons/world_weapon_testsword.tscn" id="5_cwx4m"]
|
||||
[ext_resource type="PackedScene" uid="uid://8c4l6s6x67vh" path="res://level/scenes/weapons/world_weapon_applecorer.tscn" id="6_xerh7"]
|
||||
|
||||
[sub_resource type="PlaneMesh" id="PlaneMesh_r5xs5"]
|
||||
size = Vector2(90, 90)
|
||||
@@ -46,6 +48,7 @@ color = Color(0, 0, 0, 0)
|
||||
[node name="Level" type="Node3D"]
|
||||
script = ExtResource("1_e1sh7")
|
||||
player_scene = ExtResource("1_uvcbi")
|
||||
practice_dummy_scene = ExtResource("3_i7s07")
|
||||
|
||||
[node name="Environment" type="Node3D" parent="."]
|
||||
|
||||
@@ -352,6 +355,11 @@ transform = Transform3D(15, 0, 0, 0, 15, 0, 0, 0, 15, 1.301034, -1.2294581, 2.06
|
||||
[node name="WorldWeaponSword" parent="WeaponsContainer" instance=ExtResource("5_cwx4m")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.0268106, 2.6057472, 8.836907)
|
||||
|
||||
[node name="WorldWeaponSword2" parent="WeaponsContainer" instance=ExtResource("6_xerh7")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.32151043, 5.2709904)
|
||||
|
||||
[node name="EnemiesContainer" type="Node3D" parent="."]
|
||||
|
||||
[connection signal="pressed" from="Menu/MainContainer/MainMenu/Buttons/Host" to="." method="_on_host_pressed"]
|
||||
[connection signal="pressed" from="Menu/MainContainer/MainMenu/Buttons/Join" to="." method="_on_join_pressed"]
|
||||
[connection signal="pressed" from="Menu/MainContainer/MainMenu/Option4/Quit" to="." method="_on_quit_pressed"]
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
[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")
|
||||
@@ -1,8 +1,19 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://rkvkbxlweo60"]
|
||||
[gd_scene load_steps=4 format=3 uid="uid://rkvkbxlweo60"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://culurukxpqswh" path="res://assets/Objects/TestSword.glb" id="1_4fdvi"]
|
||||
[ext_resource type="PackedScene" uid="uid://df31n55xyn27i" path="res://assets/Objects/TestSword.glb" id="1_4fdvi"]
|
||||
[ext_resource type="Script" uid="uid://jyas86y3f0jp" path="res://level/scripts/hit_box.gd" id="2_3qhqw"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_5u25i"]
|
||||
size = Vector3(0.19293976, 2.2154922, 0.5605469)
|
||||
|
||||
[node name="TestSwordMesh" type="Node3D"]
|
||||
|
||||
[node name="TestSword" parent="." instance=ExtResource("1_4fdvi")]
|
||||
transform = Transform3D(0.1, 0, 0, 0, 0.1, 0, 0, 0, 0.1, 0, 0.104662895, 0)
|
||||
|
||||
[node name="HitBox" type="Area3D" parent="."]
|
||||
script = ExtResource("2_3qhqw")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitBox"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0034065247, 1.3828986, 0.0234375)
|
||||
shape = SubResource("BoxShape3D_5u25i")
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://dyjfaq654xne3"]
|
||||
[gd_scene load_steps=4 format=3 uid="uid://dyjfaq654xne3"]
|
||||
|
||||
[ext_resource type="ArrayMesh" uid="uid://cc1kxfbkvpo2d" path="res://assets/Objects/swordlowpoly.obj" id="1"]
|
||||
[ext_resource type="ArrayMesh" uid="uid://1wnuqcx2n4xq" path="res://assets/Objects/swordlowpoly.obj" id="1"]
|
||||
[ext_resource type="Script" uid="uid://jyas86y3f0jp" path="res://level/scripts/hit_box.gd" id="2_wyi6r"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_mhdau"]
|
||||
size = Vector3(0.19293976, 1.0232315, 0.5605469)
|
||||
|
||||
[node name="SwordMesh" type="Node3D"]
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4.4839063, -2.9492104, -3.4711354)
|
||||
mesh = ExtResource("1")
|
||||
|
||||
[node name="HitBox" type="Area3D" parent="."]
|
||||
script = ExtResource("2_wyi6r")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitBox"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0034065247, 0.6794083, 0.0234375)
|
||||
shape = SubResource("BoxShape3D_mhdau")
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
[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,6 +1,6 @@
|
||||
[gd_scene load_steps=4 format=3]
|
||||
[gd_scene load_steps=4 format=3 uid="uid://byxqw8bg5da2c"]
|
||||
|
||||
[ext_resource type="Script" path="res://level/scripts/world_weapon.gd" id="1"]
|
||||
[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_sword.tres" id="2"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="1"]
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
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
|
||||
if 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
|
||||
@@ -0,0 +1 @@
|
||||
uid://base_enemy_script
|
||||
+140
-48
@@ -3,9 +3,10 @@ class_name BaseWeapon
|
||||
|
||||
## Base class for equipped weapons
|
||||
## Attached to player's hand via BoneAttachment3D
|
||||
## Provides common weapon functionality and stats
|
||||
## Uses HitBox/HurtBox system for damage detection
|
||||
|
||||
signal attack_performed()
|
||||
signal hit_connected(target: Node)
|
||||
|
||||
@export var weapon_data: WeaponData
|
||||
|
||||
@@ -13,10 +14,13 @@ signal attack_performed()
|
||||
var owner_character: Character = null
|
||||
var _mesh_instance: Node3D = null
|
||||
var _attack_timer: float = 0.0
|
||||
var _hitbox: HitBox = null
|
||||
var _is_attacking: bool = false # Prevents overlapping attacks
|
||||
|
||||
func _ready():
|
||||
if weapon_data and weapon_data.mesh_scene:
|
||||
_spawn_mesh()
|
||||
_setup_hitbox()
|
||||
|
||||
func _process(delta):
|
||||
if _attack_timer > 0:
|
||||
@@ -32,85 +36,173 @@ func _spawn_mesh():
|
||||
_mesh_instance = weapon_data.mesh_scene.instantiate()
|
||||
add_child(_mesh_instance)
|
||||
|
||||
# Check if mesh has a HitBox child, use it instead of auto-generated one
|
||||
var mesh_hitbox = _mesh_instance.get_node_or_null("HitBox")
|
||||
if mesh_hitbox and mesh_hitbox is HitBox:
|
||||
# Use the hitbox from the mesh scene
|
||||
print("[BaseWeapon] Found manual HitBox in mesh scene")
|
||||
if _hitbox:
|
||||
_hitbox.queue_free()
|
||||
_hitbox = mesh_hitbox
|
||||
_configure_hitbox()
|
||||
else:
|
||||
print("[BaseWeapon] No manual HitBox found, will auto-generate")
|
||||
|
||||
## Setup the hitbox for this weapon
|
||||
func _setup_hitbox():
|
||||
# Skip if we already have a hitbox (e.g., from mesh scene)
|
||||
if _hitbox:
|
||||
return
|
||||
|
||||
# Create hitbox dynamically based on weapon range
|
||||
_hitbox = HitBox.new()
|
||||
_hitbox.name = "HitBox"
|
||||
add_child(_hitbox)
|
||||
|
||||
# Add collision shape based on attack range - use sphere for consistent detection
|
||||
# regardless of weapon orientation during animations
|
||||
var collision = CollisionShape3D.new()
|
||||
var sphere = SphereShape3D.new()
|
||||
var range_val = weapon_data.attack_range if weapon_data else 1.5
|
||||
sphere.radius = range_val
|
||||
collision.shape = sphere
|
||||
_hitbox.add_child(collision)
|
||||
|
||||
_configure_hitbox()
|
||||
|
||||
## Configure hitbox with weapon stats and owner
|
||||
func _configure_hitbox():
|
||||
if not _hitbox:
|
||||
return
|
||||
|
||||
# Set damage stats from weapon
|
||||
if weapon_data:
|
||||
_hitbox.set_stats(weapon_data.damage, weapon_data.knockback_force)
|
||||
|
||||
# Set owner to prevent self-damage
|
||||
_hitbox.owner_entity = owner_character
|
||||
|
||||
# Connect to hit signal
|
||||
if not _hitbox.hit_landed.is_connected(_on_hitbox_hit):
|
||||
_hitbox.hit_landed.connect(_on_hitbox_hit)
|
||||
|
||||
## Called when hitbox connects with a hurtbox
|
||||
func _on_hitbox_hit(target: Node, damage_amount: float, knockback_amount: float, attacker_pos: Vector3):
|
||||
if not target or not owner_character:
|
||||
return
|
||||
|
||||
# Flash the target's hurtbox red for visual feedback
|
||||
if target is Node:
|
||||
var hurtbox = target.find_child("HurtBox", true, false)
|
||||
if hurtbox and hurtbox.has_method("flash_hit"):
|
||||
hurtbox.flash_hit()
|
||||
|
||||
hit_connected.emit(target)
|
||||
|
||||
# Route damage through server
|
||||
var attacker_id = multiplayer.get_unique_id()
|
||||
|
||||
if multiplayer.is_server():
|
||||
# We are server, apply directly
|
||||
owner_character._server_apply_damage(
|
||||
target.name,
|
||||
damage_amount,
|
||||
attacker_id,
|
||||
knockback_amount,
|
||||
attacker_pos
|
||||
)
|
||||
else:
|
||||
# Send to server
|
||||
owner_character.rpc_id(1, "_server_apply_damage",
|
||||
target.name,
|
||||
damage_amount,
|
||||
attacker_id,
|
||||
knockback_amount,
|
||||
attacker_pos
|
||||
)
|
||||
|
||||
## Perform an attack with this weapon
|
||||
## Called by the character who owns this weapon
|
||||
func perform_attack() -> bool:
|
||||
if not weapon_data or not owner_character:
|
||||
return false
|
||||
|
||||
# Check cooldown
|
||||
if _attack_timer > 0:
|
||||
# Check cooldown and if already attacking
|
||||
if _attack_timer > 0 or _is_attacking:
|
||||
return false
|
||||
|
||||
_attack_timer = weapon_data.attack_cooldown
|
||||
# Calculate total attack duration (startup + active)
|
||||
var startup = weapon_data.startup_time if weapon_data else 0.15
|
||||
var active = weapon_data.active_time if weapon_data else 0.2
|
||||
var total_duration = startup + active
|
||||
|
||||
# Set cooldown to at least cover the full attack duration
|
||||
var cooldown = max(weapon_data.attack_cooldown, total_duration)
|
||||
_attack_timer = cooldown
|
||||
_is_attacking = true
|
||||
|
||||
# Notify owner character of attack cooldown (for UI)
|
||||
if owner_character and owner_character.is_multiplayer_authority():
|
||||
owner_character._attack_timer = weapon_data.attack_cooldown
|
||||
owner_character._attack_timer = cooldown
|
||||
|
||||
# Play attack animation on owner
|
||||
# Play attack animation on owner (use weapon's animation)
|
||||
if owner_character._body:
|
||||
owner_character._body.play_attack()
|
||||
var anim_name = weapon_data.attack_animation if weapon_data.attack_animation else "Attack_OneHand"
|
||||
owner_character._body.play_attack(anim_name)
|
||||
# Sync animation to other clients
|
||||
owner_character._sync_attack_animation.rpc(anim_name)
|
||||
|
||||
# Delay damage until animation hits (roughly 70% through the animation)
|
||||
# This makes the damage apply when the swing actually connects
|
||||
var damage_delay = weapon_data.attack_cooldown * 0.4 # Adjust this multiplier to change when damage happens
|
||||
get_tree().create_timer(damage_delay).timeout.connect(_find_and_damage_targets)
|
||||
# Activate hitbox for the attack duration
|
||||
# Only activate on authority - they detect hits and send to server
|
||||
if owner_character.is_multiplayer_authority():
|
||||
_activate_hitbox()
|
||||
|
||||
attack_performed.emit()
|
||||
return true
|
||||
|
||||
## Find targets in range and apply damage
|
||||
func _find_and_damage_targets():
|
||||
if not owner_character:
|
||||
## Activate the hitbox for attack detection - waits for startup, then activates
|
||||
func _activate_hitbox():
|
||||
if not _hitbox:
|
||||
_is_attacking = false
|
||||
return
|
||||
|
||||
# Check if the owner character has authority (not this node)
|
||||
if not owner_character.is_multiplayer_authority():
|
||||
# Update owner reference in case it changed
|
||||
_hitbox.owner_entity = owner_character
|
||||
|
||||
# STARTUP PHASE - Wait before activating (wind-up animation)
|
||||
var startup = weapon_data.startup_time if weapon_data else 0.15
|
||||
if startup > 0:
|
||||
await get_tree().create_timer(startup).timeout
|
||||
|
||||
# Check if weapon/hitbox is still valid after await
|
||||
if not _hitbox or not is_instance_valid(_hitbox):
|
||||
_is_attacking = false
|
||||
return
|
||||
|
||||
var space_state = get_world_3d().direct_space_state
|
||||
var query = PhysicsShapeQueryParameters3D.new()
|
||||
var sphere = SphereShape3D.new()
|
||||
sphere.radius = weapon_data.attack_range
|
||||
query.shape = sphere
|
||||
query.transform = global_transform
|
||||
query.collision_mask = 1 # Player layer
|
||||
# ACTIVE PHASE - Hitbox on, can deal damage
|
||||
_hitbox.activate()
|
||||
|
||||
var results = space_state.intersect_shape(query)
|
||||
# Wait for active duration
|
||||
var active = weapon_data.active_time if weapon_data else 0.2
|
||||
await get_tree().create_timer(active).timeout
|
||||
|
||||
for result in results:
|
||||
var hit_body = result["collider"]
|
||||
if hit_body != owner_character and hit_body is BaseUnit:
|
||||
var attacker_id = multiplayer.get_unique_id()
|
||||
# RECOVERY PHASE - Hitbox off
|
||||
if _hitbox and is_instance_valid(_hitbox):
|
||||
_hitbox.deactivate()
|
||||
|
||||
# If we're the server, apply damage directly
|
||||
if multiplayer.is_server():
|
||||
owner_character._server_apply_damage(
|
||||
hit_body.name,
|
||||
weapon_data.damage,
|
||||
attacker_id,
|
||||
weapon_data.knockback_force,
|
||||
owner_character.global_position
|
||||
)
|
||||
else:
|
||||
# Otherwise, request server to apply damage
|
||||
owner_character.rpc_id(1, "_server_apply_damage",
|
||||
hit_body.name,
|
||||
weapon_data.damage,
|
||||
attacker_id,
|
||||
weapon_data.knockback_force,
|
||||
owner_character.global_position
|
||||
)
|
||||
break # Only hit one target per attack
|
||||
# Attack complete
|
||||
_is_attacking = false
|
||||
|
||||
## Check if weapon can attack
|
||||
func can_attack() -> bool:
|
||||
return _attack_timer <= 0
|
||||
return _attack_timer <= 0 and not _is_attacking
|
||||
|
||||
## Set the character who owns this weapon
|
||||
func set_owner_character(character: Character):
|
||||
owner_character = character
|
||||
# Update hitbox owner
|
||||
if _hitbox:
|
||||
_hitbox.owner_entity = character
|
||||
|
||||
## Get weapon stats
|
||||
func get_damage() -> float:
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
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
|
||||
|
||||
## 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
|
||||
# Don't set transform - it inherits from parent CollisionShape3D
|
||||
collision_shape.add_child(_debug_mesh)
|
||||
|
||||
func _physics_process(_delta):
|
||||
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
|
||||
|
||||
# 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
|
||||
@@ -0,0 +1 @@
|
||||
uid://jyas86y3f0jp
|
||||
@@ -0,0 +1,86 @@
|
||||
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
|
||||
|
||||
## 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):
|
||||
# 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
|
||||
# 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
|
||||
@@ -0,0 +1 @@
|
||||
uid://bj3uepduxvgju
|
||||
+225
-17
@@ -4,15 +4,19 @@ extends Node3D
|
||||
@onready var nick_input: LineEdit = $Menu/MainContainer/MainMenu/Option1/NickInput
|
||||
@onready var address_input: LineEdit = $Menu/MainContainer/MainMenu/Option3/AddressInput
|
||||
@onready var players_container: Node3D = $PlayersContainer
|
||||
@onready var weapons_container: Node3D = null # Will be created if doesn't exist
|
||||
@onready var weapons_container: Node3D = $WeaponsContainer
|
||||
@onready var enemies_container: Node3D = $EnemiesContainer
|
||||
@onready var menu: Control = $Menu
|
||||
@onready var main_menu: VBoxContainer = $Menu/MainContainer/MainMenu
|
||||
@export var player_scene: PackedScene
|
||||
@export var practice_dummy_scene: PackedScene
|
||||
|
||||
# Weapon spawning counter (server-side only)
|
||||
var _weapon_spawn_counter: int = 0
|
||||
# Track active weapons for late-join sync (server-side only)
|
||||
var _active_weapons: Dictionary = {} # weapon_id -> WorldWeapon reference
|
||||
# Track if we've already initialized to prevent double-spawning
|
||||
var _multiplayer_initialized: bool = false
|
||||
|
||||
# multiplayer chat
|
||||
@onready var message: LineEdit = $MultiplayerChat/VBoxContainer/HBoxContainer/Message
|
||||
@@ -23,6 +27,8 @@ var _active_weapons: Dictionary = {} # weapon_id -> WorldWeapon reference
|
||||
var chat_visible = false
|
||||
|
||||
func _ready():
|
||||
print("[Level] _ready() called. Peer ID: ", multiplayer.get_unique_id(), " Is server: ", multiplayer.is_server())
|
||||
|
||||
multiplayer_chat.hide()
|
||||
menu.show()
|
||||
multiplayer_chat.set_process_input(true)
|
||||
@@ -39,33 +45,83 @@ func _ready():
|
||||
add_child(weapons_container)
|
||||
print("Created WeaponsContainer")
|
||||
|
||||
# Clients: Remove manually placed weapons (server will sync them via RPC)
|
||||
if not multiplayer.is_server():
|
||||
_cleanup_manual_weapons_on_client()
|
||||
# Create or find enemies container
|
||||
if has_node("EnemiesContainer"):
|
||||
enemies_container = get_node("EnemiesContainer")
|
||||
else:
|
||||
enemies_container = Node3D.new()
|
||||
enemies_container.name = "EnemiesContainer"
|
||||
add_child(enemies_container)
|
||||
print("Created EnemiesContainer")
|
||||
|
||||
# Don't initialize weapons in _ready() - wait for Host/Join to be pressed
|
||||
# This is handled in initialize_multiplayer() which is called after the
|
||||
# multiplayer peer is properly set up
|
||||
print("[Level] _ready() complete - waiting for Host/Join")
|
||||
|
||||
func _on_connected_to_server():
|
||||
print("[Level] Connected to server! Cleaning up manual weapons")
|
||||
_cleanup_manual_weapons_on_client()
|
||||
|
||||
## Called by Network when multiplayer peer is set up
|
||||
func initialize_multiplayer():
|
||||
print("[Level] initialize_multiplayer called. is_server: ", multiplayer.is_server())
|
||||
|
||||
# Prevent double initialization
|
||||
if _multiplayer_initialized:
|
||||
print("[Level] Already initialized, skipping")
|
||||
return
|
||||
|
||||
Network.connect("player_connected", Callable(self, "_on_player_connected"))
|
||||
multiplayer.peer_disconnected.connect(_remove_player)
|
||||
_multiplayer_initialized = true
|
||||
|
||||
# Initialize any manually placed weapons in the scene
|
||||
_initialize_manual_weapons()
|
||||
if multiplayer.is_server():
|
||||
print("[Level] Running server initialization")
|
||||
Network.connect("player_connected", Callable(self, "_on_player_connected"))
|
||||
multiplayer.peer_disconnected.connect(_remove_player)
|
||||
|
||||
# Spawn initial weapons when server starts
|
||||
_spawn_initial_weapons()
|
||||
# Initialize any manually placed weapons in the scene
|
||||
_initialize_manual_weapons()
|
||||
|
||||
# Spawn initial weapons when server starts
|
||||
_spawn_initial_weapons()
|
||||
|
||||
# Spawn practice dummies
|
||||
_spawn_practice_dummies()
|
||||
|
||||
# Spawn the host player (peer ID 1)
|
||||
print("[Level] Spawning host player")
|
||||
var host_info = Network.players.get(1, {"nick": "Host", "skin": "blue"})
|
||||
_add_player(1, host_info)
|
||||
else:
|
||||
# Client initialization - clean up manual weapons immediately
|
||||
print("[Level] Running client initialization - cleaning up manual weapons")
|
||||
_cleanup_manual_weapons_on_client()
|
||||
|
||||
func _cleanup_manual_weapons_on_client():
|
||||
"""Remove manually placed weapons on clients (server will sync them via RPC)"""
|
||||
print("[Client ", multiplayer.get_unique_id(), "] _cleanup_manual_weapons_on_client called")
|
||||
|
||||
if not weapons_container:
|
||||
print("[Client] No weapons_container found!")
|
||||
return
|
||||
|
||||
print("[Client] WeaponsContainer has ", weapons_container.get_child_count(), " children")
|
||||
|
||||
var weapons_to_remove = []
|
||||
for child in weapons_container.get_children():
|
||||
if child is WorldWeapon and child.weapon_id == -1:
|
||||
weapons_to_remove.append(child)
|
||||
print("[Client] Checking child: ", child.name, " (type: ", child.get_class(), ")")
|
||||
if child is WorldWeapon:
|
||||
print("[Client] - Is WorldWeapon with weapon_id: ", child.weapon_id)
|
||||
if child.weapon_id == -1:
|
||||
weapons_to_remove.append(child)
|
||||
print("[Client] - Marked for removal")
|
||||
|
||||
print("[Client] Found ", weapons_to_remove.size(), " weapons to remove")
|
||||
for weapon in weapons_to_remove:
|
||||
print("[Client] Removing manually placed weapon: ", weapon.name)
|
||||
weapon.queue_free()
|
||||
# Use immediate removal to prevent RPC errors
|
||||
weapons_container.remove_child(weapon)
|
||||
weapon.free()
|
||||
|
||||
func _initialize_manual_weapons():
|
||||
"""Initialize any WorldWeapon nodes manually placed in the level scene"""
|
||||
@@ -144,16 +200,29 @@ func _spawn_initial_weapons():
|
||||
)
|
||||
|
||||
func _on_player_connected(peer_id, player_info):
|
||||
print("[Server] _on_player_connected called for peer ", peer_id, " with info: ", player_info)
|
||||
_add_player(peer_id, player_info)
|
||||
|
||||
# Sync existing players to the newly joined player
|
||||
if multiplayer.is_server() and peer_id != 1:
|
||||
print("[Server] Syncing existing players to newly connected peer: ", peer_id)
|
||||
# Wait a frame to ensure new player is fully initialized
|
||||
await get_tree().process_frame
|
||||
for existing_player in players_container.get_children():
|
||||
var existing_id = int(existing_player.name)
|
||||
if existing_id != peer_id: # Don't sync the player to themselves
|
||||
print("[Server] Syncing existing player ", existing_id, " to peer ", peer_id)
|
||||
rpc_id(peer_id, "_spawn_player_local", existing_id, existing_player.position)
|
||||
|
||||
# Sync existing weapons to the newly joined player (but not to server itself)
|
||||
if multiplayer.is_server() and peer_id != 1:
|
||||
print("[Server] Syncing weapons to newly connected peer: ", peer_id)
|
||||
print("[Server] Active weapons in _active_weapons: ", _active_weapons.keys())
|
||||
print("[Server] Active weapons count: ", _active_weapons.size())
|
||||
for weapon_id in _active_weapons.keys():
|
||||
var weapon = _active_weapons[weapon_id]
|
||||
if is_instance_valid(weapon) and weapon.weapon_data:
|
||||
print("[Server] Sending weapon ", weapon_id, " to peer ", peer_id)
|
||||
print("[Server] Sending weapon ", weapon_id, " (", weapon.weapon_data.weapon_name, ") at position ", weapon.global_position, " to peer ", peer_id)
|
||||
# Send current position and zero velocity for syncing
|
||||
rpc_id(peer_id, "_client_spawn_weapon",
|
||||
weapon.weapon_data.resource_path,
|
||||
@@ -161,25 +230,99 @@ func _on_player_connected(peer_id, player_info):
|
||||
Vector3.ZERO,
|
||||
weapon_id
|
||||
)
|
||||
else:
|
||||
print("[Server] Skipping invalid weapon ", weapon_id)
|
||||
|
||||
# Sync existing enemies to the newly joined player
|
||||
print("[Server] Syncing enemies to newly connected peer: ", peer_id)
|
||||
if enemies_container:
|
||||
for enemy in enemies_container.get_children():
|
||||
if enemy is BaseEnemy:
|
||||
# Extract ID from name (e.g., "PracticeDummy_1" -> 1)
|
||||
var enemy_name_parts = enemy.name.split("_")
|
||||
if enemy_name_parts.size() >= 2:
|
||||
var enemy_id = enemy_name_parts[-1].to_int()
|
||||
print("[Server] Syncing enemy ", enemy.name, " at position ", enemy.global_position, " to peer ", peer_id)
|
||||
rpc_id(peer_id, "_spawn_dummy_local", enemy_id, enemy.global_position)
|
||||
|
||||
# Sync equipped weapons for all existing players to the newly joined player
|
||||
print("[Server] Syncing equipped weapons to newly connected peer: ", peer_id)
|
||||
for player_node in players_container.get_children():
|
||||
var player = player_node as Character
|
||||
if player and is_instance_valid(player):
|
||||
# Skip the newly joined player (they don't have weapons yet)
|
||||
if int(player.name) == peer_id:
|
||||
continue
|
||||
|
||||
# Sync main hand weapon
|
||||
if player.equipped_weapon and player.equipped_weapon.weapon_data:
|
||||
print("[Server] Syncing main hand weapon for player ", player.name, " to peer ", peer_id)
|
||||
player.rpc_id(peer_id, "equip_weapon_from_world",
|
||||
player.equipped_weapon.weapon_data.resource_path)
|
||||
|
||||
# Sync off-hand weapon
|
||||
if player.equipped_offhand and player.equipped_offhand.weapon_data:
|
||||
print("[Server] Syncing off-hand weapon for player ", player.name, " to peer ", peer_id)
|
||||
player.rpc_id(peer_id, "equip_weapon_from_world",
|
||||
player.equipped_offhand.weapon_data.resource_path)
|
||||
|
||||
func _on_host_pressed():
|
||||
print("[Level] Host button pressed")
|
||||
menu.hide()
|
||||
print("[Level] Calling Network.start_host()")
|
||||
Network.start_host(nick_input.text.strip_edges(), skin_input.text.strip_edges().to_lower())
|
||||
print("[Level] Waiting one frame...")
|
||||
await get_tree().process_frame
|
||||
print("[Level] Calling initialize_multiplayer()")
|
||||
initialize_multiplayer()
|
||||
|
||||
func _on_join_pressed():
|
||||
print("[Level] Join button pressed")
|
||||
menu.hide()
|
||||
print("[Level] Calling Network.join_game()")
|
||||
Network.join_game(nick_input.text.strip_edges(), skin_input.text.strip_edges().to_lower(), address_input.text.strip_edges())
|
||||
print("[Level] Waiting one frame...")
|
||||
await get_tree().process_frame
|
||||
print("[Level] Calling initialize_multiplayer()")
|
||||
initialize_multiplayer()
|
||||
|
||||
func _add_player(id: int, player_info : Dictionary):
|
||||
print("[Level] _add_player called for peer ", id, " with info: ", player_info)
|
||||
|
||||
# Server spawns player and replicates to all clients via RPC
|
||||
if multiplayer.is_server():
|
||||
if players_container.has_node(str(id)):
|
||||
print("[Level] Player ", id, " already exists, skipping")
|
||||
return
|
||||
|
||||
var spawn_pos = get_spawn_point()
|
||||
print("[Level] Server spawning player ", id, " at ", spawn_pos)
|
||||
|
||||
# Spawn on server and all clients (call_local does both)
|
||||
rpc("_spawn_player_local", id, spawn_pos)
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func _spawn_player_local(id: int, spawn_pos: Vector3):
|
||||
if players_container.has_node(str(id)):
|
||||
print("[Peer ", multiplayer.get_unique_id(), "] Player ", id, " already exists, skipping")
|
||||
return
|
||||
|
||||
print("[Peer ", multiplayer.get_unique_id(), "] Creating player instance for peer ", id)
|
||||
var player = player_scene.instantiate()
|
||||
player.name = str(id)
|
||||
player.position = get_spawn_point()
|
||||
player.position = spawn_pos
|
||||
print("[Peer ", multiplayer.get_unique_id(), "] Adding player to PlayersContainer")
|
||||
players_container.add_child(player, true)
|
||||
print("[Peer ", multiplayer.get_unique_id(), "] Player ", id, " spawned at ", player.position)
|
||||
|
||||
var nick = Network.players[id]["nick"]
|
||||
player.nickname.text = nick
|
||||
# Get player info from Network
|
||||
var player_info = Network.players.get(id, {"nick": "Player", "skin": "blue"})
|
||||
|
||||
var nick = player_info["nick"]
|
||||
# Access nickname directly via node path since @onready hasn't loaded yet
|
||||
var nickname_label = player.get_node_or_null("PlayerNick/Nickname")
|
||||
if nickname_label:
|
||||
nickname_label.text = nick
|
||||
# player.rpc("change_nick", nick)
|
||||
|
||||
# Set up HUD for local player
|
||||
@@ -297,6 +440,63 @@ func _on_jemz_preset():
|
||||
skin_input.text = "Red"
|
||||
address_input.text = "127.0.0.1"
|
||||
|
||||
# ---------- ENEMY SPAWNING ----------
|
||||
func _spawn_practice_dummies():
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
|
||||
if not practice_dummy_scene:
|
||||
push_warning("Practice dummy scene not assigned!")
|
||||
return
|
||||
|
||||
# Wait a frame for everything to be ready
|
||||
await get_tree().process_frame
|
||||
|
||||
print("[Server] Spawning practice dummies")
|
||||
|
||||
# Spawn dummies at different positions
|
||||
var dummy_positions = [
|
||||
Vector3(10, 0, 0),
|
||||
Vector3(-10, 0, 0),
|
||||
Vector3(0, 0, 10),
|
||||
Vector3(0, 0, -10),
|
||||
]
|
||||
|
||||
var dummy_counter = 0
|
||||
for pos in dummy_positions:
|
||||
dummy_counter += 1
|
||||
rpc("_spawn_dummy_local", dummy_counter, pos)
|
||||
|
||||
## Spawn a practice dummy on all clients
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func _spawn_dummy_local(dummy_id: int, spawn_pos: Vector3):
|
||||
if not practice_dummy_scene:
|
||||
push_error("Practice dummy scene not loaded!")
|
||||
return
|
||||
|
||||
if not enemies_container:
|
||||
push_error("EnemiesContainer not found!")
|
||||
return
|
||||
|
||||
var dummy_name = "PracticeDummy_" + str(dummy_id)
|
||||
|
||||
# Don't spawn duplicates
|
||||
if enemies_container.has_node(dummy_name):
|
||||
print("[Peer ", multiplayer.get_unique_id(), "] Dummy ", dummy_name, " already exists")
|
||||
return
|
||||
|
||||
print("[Peer ", multiplayer.get_unique_id(), "] Spawning dummy ", dummy_name, " at ", spawn_pos)
|
||||
|
||||
var dummy = practice_dummy_scene.instantiate()
|
||||
dummy.name = dummy_name
|
||||
dummy.position = spawn_pos
|
||||
|
||||
# Set multiplayer authority to server
|
||||
dummy.set_multiplayer_authority(1)
|
||||
|
||||
enemies_container.add_child(dummy, true)
|
||||
print("[Peer ", multiplayer.get_unique_id(), "] Dummy ", dummy_name, " spawned successfully")
|
||||
|
||||
# ---------- WEAPON SPAWNING ----------
|
||||
## Spawn a weapon in the world (called from server, syncs to all clients)
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
@@ -356,6 +556,14 @@ func remove_world_weapon(weapon_id: int):
|
||||
print("[ERROR] remove_world_weapon called on client!")
|
||||
return
|
||||
|
||||
# Immediately remove from active weapons to prevent late-join sync issues
|
||||
if _active_weapons.has(weapon_id):
|
||||
_active_weapons.erase(weapon_id)
|
||||
print("[Server] Removed weapon ", weapon_id, " from _active_weapons. Remaining: ", _active_weapons.size())
|
||||
print("[Server] Remaining weapon IDs: ", _active_weapons.keys())
|
||||
else:
|
||||
print("[Server] WARNING: Weapon ", weapon_id, " not found in _active_weapons!")
|
||||
|
||||
# Broadcast removal to all clients
|
||||
print("[Server] Broadcasting removal RPC to all clients")
|
||||
rpc("_remove_weapon_on_clients", weapon_id)
|
||||
|
||||
@@ -16,7 +16,7 @@ func animate(_velocity: Vector3) -> void:
|
||||
return
|
||||
|
||||
# Don't override attack animation if it's playing
|
||||
if animation_player.is_playing() and animation_player.current_animation == "Attack_OneHand":
|
||||
if animation_player.is_playing() and animation_player.current_animation.begins_with("Attack"):
|
||||
return
|
||||
|
||||
# Check if we're dashing
|
||||
@@ -54,9 +54,57 @@ func _play_animation(anim_name: String):
|
||||
animation_player.play(anim_name)
|
||||
# Silently ignore if animation doesn't exist
|
||||
|
||||
func play_attack() -> void:
|
||||
func play_attack(anim_name: String = "Attack_OneHand") -> void:
|
||||
if animation_player:
|
||||
# Play attack animation once (don't loop)
|
||||
animation_player.play("Attack_OneHand", -1, 1.0)
|
||||
# Ensure it doesn't loop
|
||||
animation_player.animation_set_next("Attack_OneHand", "")
|
||||
if animation_player.has_animation(anim_name):
|
||||
animation_player.play(anim_name, -1, 1.0)
|
||||
animation_player.animation_set_next(anim_name, "")
|
||||
else:
|
||||
# Fallback to default if animation doesn't exist
|
||||
#push_warning("Animation '%s' not found, using Attack_OneHand" % anim_name)
|
||||
animation_player.play("Attack_OneHand", -1, 1.0)
|
||||
animation_player.animation_set_next("Attack_OneHand", "")
|
||||
|
||||
## Apply hue shift to all mesh instances in the character
|
||||
func set_character_color(hue_shift: float) -> void:
|
||||
# Find all MeshInstance3D children recursively
|
||||
var mesh_instances = _find_mesh_instances(self)
|
||||
|
||||
for mesh in mesh_instances:
|
||||
_apply_hue_to_mesh(mesh, hue_shift)
|
||||
|
||||
## Recursively find all MeshInstance3D nodes
|
||||
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
|
||||
|
||||
## Apply hue shift to a mesh instance
|
||||
func _apply_hue_to_mesh(mesh_instance: MeshInstance3D, hue_shift: float) -> void:
|
||||
if not mesh_instance:
|
||||
return
|
||||
|
||||
# Get or create material for each surface
|
||||
for i in range(mesh_instance.get_surface_override_material_count()):
|
||||
var material = mesh_instance.get_surface_override_material(i)
|
||||
|
||||
# If no override material, get the base material and duplicate it
|
||||
if not material:
|
||||
material = mesh_instance.mesh.surface_get_material(i)
|
||||
if material:
|
||||
material = material.duplicate()
|
||||
mesh_instance.set_surface_override_material(i, material)
|
||||
|
||||
# Apply hue shift if it's a StandardMaterial3D
|
||||
if material and material is StandardMaterial3D:
|
||||
var std_mat = material as StandardMaterial3D
|
||||
# Create a modulate color from hue
|
||||
var color = Color.from_hsv(hue_shift, 0.6, 1.0)
|
||||
std_mat.albedo_color = color
|
||||
|
||||
@@ -13,10 +13,6 @@ var player_info = {
|
||||
signal player_connected(peer_id, player_info)
|
||||
signal server_disconnected
|
||||
|
||||
func _process(_delta):
|
||||
if Input.is_action_just_pressed("quit"):
|
||||
get_tree().quit(0)
|
||||
|
||||
func _ready() -> void:
|
||||
multiplayer.server_disconnected.connect(_on_connection_failed)
|
||||
multiplayer.connection_failed.connect(_on_server_disconnected)
|
||||
|
||||
+168
-67
@@ -42,7 +42,13 @@ var is_blocking: bool = false
|
||||
@export var attack_damage: float = 10.0
|
||||
@export var attack_range: float = 3.0
|
||||
@export var attack_cooldown: float = 0.5
|
||||
@export var unarmed_knockback: float = 5.0
|
||||
@export_category("Unarmed Attack Timing")
|
||||
@export var unarmed_startup: float = 0.1 # Wind-up before hit
|
||||
@export var unarmed_active: float = 0.15 # Hit window duration
|
||||
var _attack_timer: float = 0.0
|
||||
var _unarmed_hitbox: HitBox = null
|
||||
var _is_unarmed_attacking: bool = false # Prevents overlapping unarmed attacks
|
||||
|
||||
# Dash system
|
||||
@export var dash_speed_multiplier: float = 2.0
|
||||
@@ -64,7 +70,10 @@ func _enter_tree():
|
||||
|
||||
func _ready():
|
||||
super._ready()
|
||||
set_respawn_point(Vector3(0, 5, 0))
|
||||
# Set respawn point to current position (where we spawned) - base_unit._ready already does this
|
||||
# Don't override with a hardcoded position
|
||||
|
||||
print("[Player ", name, "] _ready called. Authority: ", is_multiplayer_authority(), " Position: ", global_position)
|
||||
|
||||
# Capture mouse for local player
|
||||
if is_multiplayer_authority():
|
||||
@@ -72,13 +81,18 @@ func _ready():
|
||||
|
||||
# Auto-find body node (needed for instanced scenes where @export NodePath doesn't work reliably)
|
||||
if _body == null:
|
||||
for child in get_children():
|
||||
if child.name == "Armature" or child.name == "3DGodotRobot":
|
||||
_body = child
|
||||
print("Auto-found _body: ", child.name)
|
||||
break
|
||||
if _body == null:
|
||||
push_error("Could not find body node (Armature or 3DGodotRobot)!")
|
||||
# Try specific paths first
|
||||
if has_node("LilguyRigged/Armature"):
|
||||
_body = get_node("LilguyRigged/Armature")
|
||||
print("Auto-found _body: LilguyRigged/Armature")
|
||||
elif has_node("Armature"):
|
||||
_body = get_node("Armature")
|
||||
print("Auto-found _body: Armature")
|
||||
elif has_node("3DGodotRobot"):
|
||||
_body = get_node("3DGodotRobot")
|
||||
print("Auto-found _body: 3DGodotRobot")
|
||||
else:
|
||||
push_error("Could not find body node!")
|
||||
|
||||
# Auto-find spring arm offset
|
||||
if _spring_arm_offset == null:
|
||||
@@ -144,6 +158,9 @@ func _ready():
|
||||
# Setup weapon pickup detection area
|
||||
_setup_weapon_pickup_area()
|
||||
|
||||
# Setup unarmed attack hitbox (deferred to avoid multiplayer timing issues)
|
||||
call_deferred("_setup_unarmed_hitbox")
|
||||
|
||||
# Auto-find weapon attachment if not set
|
||||
if _weapon_attachment == null:
|
||||
var bone_attach = get_node_or_null("3DGodotRobot/RobotArmature/Skeleton3D/BoneAttachment3D")
|
||||
@@ -189,19 +206,19 @@ func _physics_process(delta):
|
||||
freeze()
|
||||
return
|
||||
|
||||
# Apply gravity when not on floor
|
||||
if not is_on_floor():
|
||||
velocity.y -= gravity * delta
|
||||
_body.animate(velocity)
|
||||
|
||||
if is_on_floor():
|
||||
if Input.is_action_just_pressed("jump"):
|
||||
velocity.y = JUMP_VELOCITY
|
||||
else:
|
||||
velocity.y -= gravity * delta
|
||||
# Handle jump
|
||||
if is_on_floor() and Input.is_action_just_pressed("jump"):
|
||||
velocity.y = JUMP_VELOCITY
|
||||
|
||||
_move()
|
||||
move_and_slide()
|
||||
_body.animate(velocity)
|
||||
|
||||
if _body:
|
||||
_body.animate(velocity)
|
||||
|
||||
func _process(delta):
|
||||
# Check if multiplayer is ready
|
||||
@@ -269,14 +286,16 @@ func freeze():
|
||||
velocity.x = 0
|
||||
velocity.z = 0
|
||||
_current_speed = 0
|
||||
_body.animate(Vector3.ZERO)
|
||||
if _body:
|
||||
_body.animate(Vector3.ZERO)
|
||||
|
||||
func _move() -> void:
|
||||
# If dashing, use dash movement
|
||||
if _is_dashing:
|
||||
velocity.x = _dash_direction.x * _current_speed * dash_speed_multiplier
|
||||
velocity.z = _dash_direction.z * _current_speed * dash_speed_multiplier
|
||||
_body.apply_rotation(velocity)
|
||||
if _body:
|
||||
_body.apply_rotation(velocity)
|
||||
return
|
||||
|
||||
var _input_direction: Vector2 = Vector2.ZERO
|
||||
@@ -289,12 +308,14 @@ func _move() -> void:
|
||||
var _direction: Vector3 = transform.basis * Vector3(_input_direction.x, 0, _input_direction.y).normalized()
|
||||
|
||||
is_running()
|
||||
_direction = _direction.rotated(Vector3.UP, _spring_arm_offset.rotation.y)
|
||||
if _spring_arm_offset:
|
||||
_direction = _direction.rotated(Vector3.UP, _spring_arm_offset.rotation.y)
|
||||
|
||||
if _direction:
|
||||
velocity.x = _direction.x * _current_speed
|
||||
velocity.z = _direction.z * _current_speed
|
||||
_body.apply_rotation(velocity)
|
||||
if _body:
|
||||
_body.apply_rotation(velocity)
|
||||
return
|
||||
|
||||
velocity.x = move_toward(velocity.x, 0, _current_speed)
|
||||
@@ -328,12 +349,27 @@ func get_texture_from_name(skin_color: SkinColor) -> CompressedTexture2D:
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func set_player_skin(skin_name: SkinColor) -> void:
|
||||
var texture = get_texture_from_name(skin_name)
|
||||
# Check if we're using the LilguyRigged model
|
||||
if _body is LilguyBody:
|
||||
# Use hue-based color system for Lilguy
|
||||
var hue = _get_hue_from_skin_color(skin_name)
|
||||
_body.set_character_color(hue)
|
||||
else:
|
||||
# Use texture-based system for 3DGodotRobot
|
||||
var texture = get_texture_from_name(skin_name)
|
||||
set_mesh_texture(_bottom_mesh, texture)
|
||||
set_mesh_texture(_chest_mesh, texture)
|
||||
set_mesh_texture(_face_mesh, texture)
|
||||
set_mesh_texture(_limbs_head_mesh, texture)
|
||||
|
||||
set_mesh_texture(_bottom_mesh, texture)
|
||||
set_mesh_texture(_chest_mesh, texture)
|
||||
set_mesh_texture(_face_mesh, texture)
|
||||
set_mesh_texture(_limbs_head_mesh, texture)
|
||||
## Convert SkinColor enum to hue value (0.0 to 1.0)
|
||||
func _get_hue_from_skin_color(skin_color: SkinColor) -> float:
|
||||
match skin_color:
|
||||
SkinColor.BLUE: return 0.6 # Blue hue
|
||||
SkinColor.GREEN: return 0.33 # Green hue
|
||||
SkinColor.RED: return 0.0 # Red hue
|
||||
SkinColor.YELLOW: return 0.16 # Yellow hue
|
||||
_: return 0.6 # Default to blue
|
||||
|
||||
func set_mesh_texture(mesh_instance: MeshInstance3D, texture: CompressedTexture2D) -> void:
|
||||
if mesh_instance:
|
||||
@@ -392,41 +428,26 @@ func _perform_attack():
|
||||
|
||||
# Fallback to default unarmed attack
|
||||
# Don't attack if already attacking
|
||||
if _body and _body.animation_player and _body.animation_player.current_animation == "Attack1":
|
||||
if _body and _body.animation_player and _body.animation_player.current_animation.begins_with("Attack"):
|
||||
return
|
||||
|
||||
if _attack_timer > 0:
|
||||
if _attack_timer > 0 or _is_unarmed_attacking:
|
||||
return
|
||||
|
||||
_attack_timer = attack_cooldown
|
||||
# Calculate total attack duration and ensure cooldown covers it
|
||||
var total_duration = unarmed_startup + unarmed_active
|
||||
var cooldown = max(attack_cooldown, total_duration)
|
||||
_attack_timer = cooldown
|
||||
_is_unarmed_attacking = true
|
||||
|
||||
# Play attack animation once
|
||||
if _body:
|
||||
_body.play_attack()
|
||||
_body.play_attack("Attack_OneHand")
|
||||
# Sync animation to other clients
|
||||
_sync_attack_animation.rpc("Attack_OneHand")
|
||||
|
||||
# Find nearest enemy in range
|
||||
var space_state = get_world_3d().direct_space_state
|
||||
var query = PhysicsShapeQueryParameters3D.new()
|
||||
var sphere = SphereShape3D.new()
|
||||
sphere.radius = attack_range
|
||||
query.shape = sphere
|
||||
query.transform = global_transform
|
||||
query.collision_mask = 1 # Player layer
|
||||
|
||||
var results = space_state.intersect_shape(query)
|
||||
|
||||
for result in results:
|
||||
var hit_body = result["collider"]
|
||||
if hit_body != self and hit_body is BaseUnit:
|
||||
var attacker_id = multiplayer.get_unique_id()
|
||||
|
||||
# If we're the server, apply damage directly (default unarmed knockback)
|
||||
if multiplayer.is_server():
|
||||
_server_apply_damage(hit_body.name, attack_damage, attacker_id, 5.0, global_position)
|
||||
else:
|
||||
# Otherwise, request server to apply damage
|
||||
rpc_id(1, "_server_apply_damage", hit_body.name, attack_damage, attacker_id, 5.0, global_position)
|
||||
break # Only hit one target per attack
|
||||
# Activate unarmed hitbox for damage detection
|
||||
_activate_unarmed_hitbox()
|
||||
|
||||
## Server-side damage application
|
||||
@rpc("any_peer", "reliable")
|
||||
@@ -434,16 +455,25 @@ func _server_apply_damage(target_name: String, damage: float, attacker_id: int,
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
|
||||
# Get the target from the players container
|
||||
var level = get_tree().get_current_scene()
|
||||
if not level or not level.has_node("PlayersContainer"):
|
||||
if not level:
|
||||
return
|
||||
|
||||
var players_container = level.get_node("PlayersContainer")
|
||||
if not players_container.has_node(target_name):
|
||||
return
|
||||
var target = null
|
||||
|
||||
var target = players_container.get_node(target_name)
|
||||
# Check players container first
|
||||
if level.has_node("PlayersContainer"):
|
||||
var players_container = level.get_node("PlayersContainer")
|
||||
if players_container.has_node(target_name):
|
||||
target = players_container.get_node(target_name)
|
||||
|
||||
# If not found in players, check enemies container
|
||||
if not target and level.has_node("EnemiesContainer"):
|
||||
var enemies_container = level.get_node("EnemiesContainer")
|
||||
if enemies_container.has_node(target_name):
|
||||
target = enemies_container.get_node(target_name)
|
||||
|
||||
# Apply damage if target found
|
||||
if target and target is BaseUnit:
|
||||
target.take_damage(damage, attacker_id, knockback, attacker_pos)
|
||||
|
||||
@@ -462,11 +492,13 @@ func _on_health_changed(_old_health: float, _new_health: float):
|
||||
_update_health_display()
|
||||
|
||||
func _on_died(killer_id: int):
|
||||
# Disable player when dead
|
||||
set_physics_process(false)
|
||||
set_process(false)
|
||||
print("[Player ", name, "] _on_died called. Authority: ", is_multiplayer_authority())
|
||||
# Only the authority should disable their own processing
|
||||
if is_multiplayer_authority():
|
||||
set_physics_process(false)
|
||||
set_process(false)
|
||||
|
||||
# Visual feedback - could add death animation here
|
||||
# Visual feedback - runs on all peers
|
||||
if _body:
|
||||
_body.visible = false
|
||||
|
||||
@@ -483,18 +515,19 @@ func _on_died(killer_id: int):
|
||||
get_node("HealthUI/HealthText").text = "DEAD - Respawning..."
|
||||
|
||||
func _on_respawned():
|
||||
# Re-enable player
|
||||
set_physics_process(true)
|
||||
set_process(true)
|
||||
print("[Player ", name, "] _on_respawned called. Authority: ", is_multiplayer_authority(), " Position: ", global_position)
|
||||
# Only the authority should re-enable their own processing
|
||||
if is_multiplayer_authority():
|
||||
set_physics_process(true)
|
||||
set_process(true)
|
||||
print("[Player ", name, "] Re-enabled physics processing")
|
||||
|
||||
# Visual feedback - runs on all peers
|
||||
if _body:
|
||||
_body.visible = true
|
||||
|
||||
_update_health_display()
|
||||
|
||||
if is_multiplayer_authority():
|
||||
print("You respawned!")
|
||||
|
||||
## Dash system
|
||||
func _perform_dash():
|
||||
if not is_multiplayer_authority() or is_dead or not is_on_floor():
|
||||
@@ -524,6 +557,12 @@ func _perform_dash():
|
||||
|
||||
# Animation is handled by the Body's animate function (Jump animation plays during dash)
|
||||
|
||||
## Sync attack animation to all clients
|
||||
@rpc("any_peer", "call_remote", "unreliable")
|
||||
func _sync_attack_animation(anim_name: String):
|
||||
if _body:
|
||||
_body.play_attack(anim_name)
|
||||
|
||||
## Override hurt animation from BaseUnit
|
||||
func _play_hurt_animation():
|
||||
if _body and _body.animation_player:
|
||||
@@ -577,6 +616,68 @@ func _setup_weapon_pickup_area():
|
||||
pickup_area.area_entered.connect(_on_weapon_area_entered)
|
||||
pickup_area.area_exited.connect(_on_weapon_area_exited)
|
||||
|
||||
func _setup_unarmed_hitbox():
|
||||
# Create hitbox for unarmed attacks
|
||||
_unarmed_hitbox = HitBox.new()
|
||||
_unarmed_hitbox.name = "UnarmedHitBox"
|
||||
_unarmed_hitbox.owner_entity = self
|
||||
_unarmed_hitbox.set_stats(attack_damage, unarmed_knockback)
|
||||
|
||||
# Attach to body so it rotates with player facing direction
|
||||
if _body:
|
||||
_body.add_child(_unarmed_hitbox)
|
||||
else:
|
||||
add_child(_unarmed_hitbox)
|
||||
|
||||
# Add collision shape - larger sphere in front of player
|
||||
var collision = CollisionShape3D.new()
|
||||
var sphere = SphereShape3D.new()
|
||||
sphere.radius = attack_range # Full attack range as radius
|
||||
collision.shape = sphere
|
||||
# Position in front of player (Z is forward for the body)
|
||||
collision.position = Vector3(0, 0.8, -attack_range * 0.75)
|
||||
_unarmed_hitbox.add_child(collision)
|
||||
|
||||
# Connect hit signal
|
||||
_unarmed_hitbox.hit_landed.connect(_on_unarmed_hit)
|
||||
|
||||
func _on_unarmed_hit(target: Node, damage_amount: float, knockback_amount: float, attacker_pos: Vector3):
|
||||
if not target:
|
||||
return
|
||||
|
||||
# Route damage through server
|
||||
var attacker_id = multiplayer.get_unique_id()
|
||||
|
||||
if multiplayer.is_server():
|
||||
_server_apply_damage(target.name, damage_amount, attacker_id, knockback_amount, attacker_pos)
|
||||
else:
|
||||
rpc_id(1, "_server_apply_damage", target.name, damage_amount, attacker_id, knockback_amount, attacker_pos)
|
||||
|
||||
func _activate_unarmed_hitbox():
|
||||
if not _unarmed_hitbox:
|
||||
_is_unarmed_attacking = false
|
||||
return
|
||||
|
||||
# STARTUP PHASE - Wait before activating (wind-up animation)
|
||||
if unarmed_startup > 0:
|
||||
await get_tree().create_timer(unarmed_startup).timeout
|
||||
|
||||
if not _unarmed_hitbox or not is_instance_valid(_unarmed_hitbox):
|
||||
_is_unarmed_attacking = false
|
||||
return
|
||||
|
||||
# ACTIVE PHASE - Hitbox on, can deal damage
|
||||
_unarmed_hitbox.activate()
|
||||
|
||||
await get_tree().create_timer(unarmed_active).timeout
|
||||
|
||||
# RECOVERY PHASE - Hitbox off
|
||||
if _unarmed_hitbox and is_instance_valid(_unarmed_hitbox):
|
||||
_unarmed_hitbox.deactivate()
|
||||
|
||||
# Attack complete
|
||||
_is_unarmed_attacking = false
|
||||
|
||||
func _on_weapon_area_entered(area: Area3D):
|
||||
# Check if the area belongs to a WorldWeapon
|
||||
var weapon = area.get_parent()
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
_update_health_display()
|
||||
print("[PracticeDummy] Respawned!")
|
||||
@@ -0,0 +1 @@
|
||||
uid://practice_dummy_script
|
||||
@@ -18,6 +18,14 @@ enum Hand { MAIN_HAND, OFF_HAND, TWO_HAND }
|
||||
@export var attack_animation: String = "Attack1" # Animation to play when attacking
|
||||
@export var knockback_force: float = 8.0 # How much to push the target back
|
||||
|
||||
@export_category("Attack Timing")
|
||||
## Time before hitbox activates (wind-up/anticipation)
|
||||
@export var startup_time: float = 0.15
|
||||
## Duration hitbox stays active (the actual hit window)
|
||||
@export var active_time: float = 0.2
|
||||
## Time after hitbox deactivates (follow-through, can't act)
|
||||
## Note: recovery_time = attack_cooldown - startup_time - active_time (calculated automatically)
|
||||
|
||||
@export_category("Defense Stats")
|
||||
@export var can_block: bool = false
|
||||
@export_range(0.0, 1.0) var block_reduction: float = 0.5 # Percentage of damage blocked (0.5 = 50%)
|
||||
|
||||
@@ -100,3 +100,5 @@ toggle_character_sheet={
|
||||
3d_physics/layer_1="player"
|
||||
3d_physics/layer_2="world"
|
||||
3d_physics/layer_3="weapon"
|
||||
3d_physics/layer_4="hitbox"
|
||||
3d_physics/layer_5="hurtbox"
|
||||
|
||||
Reference in New Issue
Block a user