Compare commits

..
9 Commits
Author SHA1 Message Date
Twirpytherobot ddc1522174 weapon timings 2025-11-28 18:40:23 +00:00
Twirpytherobot 7d18de1621 hitbox changes 2025-11-28 18:33:51 +00:00
Twirpytherobot 10cc8720b7 Hithurtboxes 2025-11-21 23:53:28 +00:00
Twirpytherobot 2ec7d56511 Sword animations now sync 2025-11-21 22:59:41 +00:00
Twirpytherobot b1f1016475 Applecorer 2025-11-21 22:48:05 +00:00
Twirpytherobot e192802f01 Finally fixed the sync issue!
Late joining now syncs properly!!!!!!
2025-11-19 23:12:12 +00:00
Twirpytherobot 85e76d67ca Fixed yo shit 2025-11-17 19:26:56 +00:00
Scottlg 6e4e3a3585 Revert to using player.tscn (robot character) temporarily
- Changed level.tscn back to player.tscn instead of Player_Lilguy.tscn
- This ensures the Character script is properly attached
- Lilguy character integration will be done properly later in Godot editor
- Game now runs without type errors
2025-11-16 23:30:48 +00:00
Scottlg bdc507e852 Fix nickname access error on player instantiation
- Changed from player.nickname.text to get_node_or_null approach
- @onready variables aren't available immediately after instantiation
- Prevents 'Invalid access to property or key nickname' error
2025-11-16 23:29:30 +00:00
23 changed files with 767 additions and 154 deletions
Binary file not shown.
+18
View File
@@ -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
+2
View File
@@ -12,6 +12,8 @@ damage = 8.0
attack_range = 2.5 attack_range = 2.5
attack_cooldown = 0.8 attack_cooldown = 0.8
knockback_force = 15.0 knockback_force = 15.0
startup_time = 0.25
active_time = 0.15
can_block = true can_block = true
block_reduction = 0.7 block_reduction = 0.7
mesh_scene = ExtResource("2") mesh_scene = ExtResource("2")
+2
View File
@@ -12,6 +12,8 @@ attack_range = 3.5
attack_cooldown = 0.6 attack_cooldown = 0.6
knockback_force = 12.0 knockback_force = 12.0
attack_animation = "Attack1" attack_animation = "Attack1"
startup_time = 0.12
active_time = 0.2
mesh_scene = ExtResource("2") mesh_scene = ExtResource("2")
pickup_radius = 1.5 pickup_radius = 1.5
weight = 2.0 weight = 2.0
+2
View File
@@ -11,5 +11,7 @@ damage = 20.0
attack_range = 3.5 attack_range = 3.5
attack_cooldown = 0.6 attack_cooldown = 0.6
knockback_force = 12.0 knockback_force = 12.0
startup_time = 0.12
active_time = 1.0
mesh_scene = ExtResource("1_gdc1w") mesh_scene = ExtResource("1_gdc1w")
weight = 2.0 weight = 2.0
+49 -22
View File
@@ -1,37 +1,52 @@
[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://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"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_yxyay"]
radius = 0.35796 radius = 0.35796
height = 1.73092 height = 1.73092
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_hurtbox"]
radius = 0.4
height = 1.8
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_xbohm"] [sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_xbohm"]
properties/0/path = NodePath(".:position") properties/0/path = NodePath(".:position")
properties/0/spawn = true properties/0/spawn = true
properties/0/replication_mode = 1 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/spawn = true
properties/1/replication_mode = 1 properties/1/replication_mode = 1
properties/2/path = NodePath("PlayerNick/Nickname:text") properties/2/path = NodePath("PlayerNick/Nickname:text")
properties/2/spawn = true properties/2/spawn = true
properties/2/replication_mode = 1 properties/2/replication_mode = 1
properties/3/path = NodePath("Armature:rotation") properties/3/path = NodePath("LilguyRigged/Armature:rotation")
properties/3/spawn = true properties/3/spawn = true
properties/3/replication_mode = 1 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 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) 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") script = ExtResource("3_body")
_character = NodePath("..") _character = NodePath("../..")
animation_player = NodePath("../AnimationPlayer") animation_player = NodePath("../AnimationPlayer")
[node name="Skeleton3D" parent="Armature" index="0"] [node name="Skeleton3D" parent="LilguyRigged/Armature" index="0"]
bones/0/position = Vector3(-0.32852697, 2.914154, -546.76843) bones/0/position = Vector3(-0.32852697, 2.914154, -546.76843)
bones/0/rotation = Quaternion(-0.6608288, 0.28933647, -0.19178489, 0.6654384) bones/0/rotation = Quaternion(-0.6608288, 0.28933647, -0.19178489, 0.6654384)
bones/1/position = Vector3(0.054167695, 63.219894, -3.33786e-06) bones/1/position = Vector3(0.054167695, 63.219894, -3.33786e-06)
@@ -78,45 +93,57 @@ 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.009521758, -0.0077993367, 0.9928351)
bones/24/position = Vector3(-2.4847686e-06, 11.913359, -6.198885e-06) bones/24/position = Vector3(-2.4847686e-06, 11.913359, -6.198885e-06)
[node name="WeaponPoint" type="BoneAttachment3D" parent="Armature/Skeleton3D" index="1"] [node name="WeaponPoint" type="BoneAttachment3D" parent="LilguyRigged/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) 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)
bone_name = "mixamorig_RightHand" bone_name = "mixamorig_RightHand"
bone_idx = 14 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) 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"] [node name="OffhandPoint" type="BoneAttachment3D" parent="LilguyRigged/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) 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)
bone_name = "mixamorig_LeftHand" bone_name = "mixamorig_LeftHand"
bone_idx = 10 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) 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"] [node name="CollisionShape3D" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.066, 0.828, 0.01) transform = Transform3D(2, 0, 0, 0, 2, 0, 0, 0, 2, -0.066, 1.647685, 0.01)
shape = SubResource("CapsuleShape3D_yxyay") 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) 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") _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) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
spring_length = 5.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 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) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.97037, 0)
billboard = 1 billboard = 1
outline_modulate = Color(0, 0, 0, 0.301961) outline_modulate = Color(0, 0, 0, 0.301961)
text = "player name test" text = "player name test"
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="." index="5"] [node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
replication_config = SubResource("SceneReplicationConfig_xbohm") replication_config = SubResource("SceneReplicationConfig_xbohm")
[editable path="LilguyRigged"]
+7 -3
View File
@@ -1,10 +1,11 @@
[gd_scene load_steps=19 format=3 uid="uid://dugaivbj1o66n"] [gd_scene load_steps=20 format=3 uid="uid://dugaivbj1o66n"]
[ext_resource type="Script" uid="uid://d0dgljwwl463n" path="res://level/scripts/level.gd" id="1_e1sh7"] [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="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="FontFile" uid="uid://wipqjhfqeuwd" 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" 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://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"] [sub_resource type="PlaneMesh" id="PlaneMesh_r5xs5"]
size = Vector2(90, 90) size = Vector2(90, 90)
@@ -352,6 +353,9 @@ 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")] [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) 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)
[connection signal="pressed" from="Menu/MainContainer/MainMenu/Buttons/Host" to="." method="_on_host_pressed"] [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/Buttons/Join" to="." method="_on_join_pressed"]
[connection signal="pressed" from="Menu/MainContainer/MainMenu/Option4/Quit" to="." method="_on_quit_pressed"] [connection signal="pressed" from="Menu/MainContainer/MainMenu/Option4/Quit" to="." method="_on_quit_pressed"]
+19
View File
@@ -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")
+13 -2
View File
@@ -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="TestSwordMesh" type="Node3D"]
[node name="TestSword" parent="." instance=ExtResource("1_4fdvi")] [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) 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")
+13 -2
View File
@@ -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="SwordMesh" type="Node3D"]
[node name="MeshInstance3D" type="MeshInstance3D" parent="."] [node name="MeshInstance3D" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4.4839063, -2.9492104, -3.4711354) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4.4839063, -2.9492104, -3.4711354)
mesh = ExtResource("1") 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")
+2 -2
View File
@@ -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"] [ext_resource type="Resource" path="res://level/resources/weapon_sword.tres" id="2"]
[sub_resource type="BoxShape3D" id="1"] [sub_resource type="BoxShape3D" id="1"]
+116 -44
View File
@@ -3,9 +3,10 @@ class_name BaseWeapon
## Base class for equipped weapons ## Base class for equipped weapons
## Attached to player's hand via BoneAttachment3D ## Attached to player's hand via BoneAttachment3D
## Provides common weapon functionality and stats ## Uses HitBox/HurtBox system for damage detection
signal attack_performed() signal attack_performed()
signal hit_connected(target: Node)
@export var weapon_data: WeaponData @export var weapon_data: WeaponData
@@ -13,10 +14,12 @@ signal attack_performed()
var owner_character: Character = null var owner_character: Character = null
var _mesh_instance: Node3D = null var _mesh_instance: Node3D = null
var _attack_timer: float = 0.0 var _attack_timer: float = 0.0
var _hitbox: HitBox = null
func _ready(): func _ready():
if weapon_data and weapon_data.mesh_scene: if weapon_data and weapon_data.mesh_scene:
_spawn_mesh() _spawn_mesh()
_setup_hitbox()
func _process(delta): func _process(delta):
if _attack_timer > 0: if _attack_timer > 0:
@@ -32,6 +35,91 @@ func _spawn_mesh():
_mesh_instance = weapon_data.mesh_scene.instantiate() _mesh_instance = weapon_data.mesh_scene.instantiate()
add_child(_mesh_instance) 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 ## Perform an attack with this weapon
## Called by the character who owns this weapon ## Called by the character who owns this weapon
func perform_attack() -> bool: func perform_attack() -> bool:
@@ -48,61 +136,42 @@ func perform_attack() -> bool:
if owner_character and owner_character.is_multiplayer_authority(): if owner_character and owner_character.is_multiplayer_authority():
owner_character._attack_timer = weapon_data.attack_cooldown owner_character._attack_timer = weapon_data.attack_cooldown
# Play attack animation on owner # Play attack animation on owner (use weapon's animation)
if owner_character._body: 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) # Activate hitbox for the attack duration
# This makes the damage apply when the swing actually connects # Only activate on authority - they detect hits and send to server
var damage_delay = weapon_data.attack_cooldown * 0.4 # Adjust this multiplier to change when damage happens if owner_character.is_multiplayer_authority():
get_tree().create_timer(damage_delay).timeout.connect(_find_and_damage_targets) _activate_hitbox()
attack_performed.emit() attack_performed.emit()
return true return true
## Find targets in range and apply damage ## Activate the hitbox for attack detection - active for entire animation
func _find_and_damage_targets(): func _activate_hitbox():
if not owner_character: if not _hitbox:
return return
# Check if the owner character has authority (not this node) # Update owner reference in case it changed
if not owner_character.is_multiplayer_authority(): _hitbox.owner_entity = owner_character
return
var space_state = get_world_3d().direct_space_state # Activate hitbox immediately for the duration specified in weapon data
var query = PhysicsShapeQueryParameters3D.new() _hitbox.activate()
var sphere = SphereShape3D.new()
sphere.radius = weapon_data.attack_range
query.shape = sphere
query.transform = global_transform
query.collision_mask = 1 # Player layer
var results = space_state.intersect_shape(query) # Calculate total active duration from weapon resource (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 duration = startup + active
for result in results: await get_tree().create_timer(duration).timeout
var hit_body = result["collider"]
if hit_body != owner_character and hit_body is BaseUnit:
var attacker_id = multiplayer.get_unique_id()
# If we're the server, apply damage directly # Deactivate when attack is complete
if multiplayer.is_server(): if _hitbox and is_instance_valid(_hitbox):
owner_character._server_apply_damage( _hitbox.deactivate()
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
## Check if weapon can attack ## Check if weapon can attack
func can_attack() -> bool: func can_attack() -> bool:
@@ -111,6 +180,9 @@ func can_attack() -> bool:
## Set the character who owns this weapon ## Set the character who owns this weapon
func set_owner_character(character: Character): func set_owner_character(character: Character):
owner_character = character owner_character = character
# Update hitbox owner
if _hitbox:
_hitbox.owner_entity = character
## Get weapon stats ## Get weapon stats
func get_damage() -> float: func get_damage() -> float:
+137
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://jyas86y3f0jp
+86
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://bj3uepduxvgju
+133 -8
View File
@@ -13,6 +13,8 @@ extends Node3D
var _weapon_spawn_counter: int = 0 var _weapon_spawn_counter: int = 0
# Track active weapons for late-join sync (server-side only) # Track active weapons for late-join sync (server-side only)
var _active_weapons: Dictionary = {} # weapon_id -> WorldWeapon reference 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 # multiplayer chat
@onready var message: LineEdit = $MultiplayerChat/VBoxContainer/HBoxContainer/Message @onready var message: LineEdit = $MultiplayerChat/VBoxContainer/HBoxContainer/Message
@@ -23,6 +25,8 @@ var _active_weapons: Dictionary = {} # weapon_id -> WorldWeapon reference
var chat_visible = false var chat_visible = false
func _ready(): func _ready():
print("[Level] _ready() called. Peer ID: ", multiplayer.get_unique_id(), " Is server: ", multiplayer.is_server())
multiplayer_chat.hide() multiplayer_chat.hide()
menu.show() menu.show()
multiplayer_chat.set_process_input(true) multiplayer_chat.set_process_input(true)
@@ -39,11 +43,28 @@ func _ready():
add_child(weapons_container) add_child(weapons_container)
print("Created WeaponsContainer") print("Created WeaponsContainer")
# Clients: Remove manually placed weapons (server will sync them via RPC) # Don't initialize weapons in _ready() - wait for Host/Join to be pressed
if not multiplayer.is_server(): # 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() _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 return
_multiplayer_initialized = true
if multiplayer.is_server():
print("[Level] Running server initialization")
Network.connect("player_connected", Callable(self, "_on_player_connected")) Network.connect("player_connected", Callable(self, "_on_player_connected"))
multiplayer.peer_disconnected.connect(_remove_player) multiplayer.peer_disconnected.connect(_remove_player)
@@ -53,19 +74,40 @@ func _ready():
# Spawn initial weapons when server starts # Spawn initial weapons when server starts
_spawn_initial_weapons() _spawn_initial_weapons()
# 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(): func _cleanup_manual_weapons_on_client():
"""Remove manually placed weapons on clients (server will sync them via RPC)""" """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: if not weapons_container:
print("[Client] No weapons_container found!")
return return
print("[Client] WeaponsContainer has ", weapons_container.get_child_count(), " children")
var weapons_to_remove = [] var weapons_to_remove = []
for child in weapons_container.get_children(): for child in weapons_container.get_children():
if child is WorldWeapon and child.weapon_id == -1: 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) 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: for weapon in weapons_to_remove:
print("[Client] Removing manually placed weapon: ", weapon.name) 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(): func _initialize_manual_weapons():
"""Initialize any WorldWeapon nodes manually placed in the level scene""" """Initialize any WorldWeapon nodes manually placed in the level scene"""
@@ -144,16 +186,29 @@ func _spawn_initial_weapons():
) )
func _on_player_connected(peer_id, player_info): 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) _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) # Sync existing weapons to the newly joined player (but not to server itself)
if multiplayer.is_server() and peer_id != 1: if multiplayer.is_server() and peer_id != 1:
print("[Server] Syncing weapons to newly connected peer: ", peer_id) 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()) print("[Server] Active weapons count: ", _active_weapons.size())
for weapon_id in _active_weapons.keys(): for weapon_id in _active_weapons.keys():
var weapon = _active_weapons[weapon_id] var weapon = _active_weapons[weapon_id]
if is_instance_valid(weapon) and weapon.weapon_data: 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 # Send current position and zero velocity for syncing
rpc_id(peer_id, "_client_spawn_weapon", rpc_id(peer_id, "_client_spawn_weapon",
weapon.weapon_data.resource_path, weapon.weapon_data.resource_path,
@@ -161,25 +216,87 @@ func _on_player_connected(peer_id, player_info):
Vector3.ZERO, Vector3.ZERO,
weapon_id weapon_id
) )
else:
print("[Server] Skipping invalid weapon ", weapon_id)
# 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(): func _on_host_pressed():
print("[Level] Host button pressed")
menu.hide() menu.hide()
print("[Level] Calling Network.start_host()")
Network.start_host(nick_input.text.strip_edges(), skin_input.text.strip_edges().to_lower()) 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(): func _on_join_pressed():
print("[Level] Join button pressed")
menu.hide() 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()) 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): 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)): if players_container.has_node(str(id)):
print("[Level] Player ", id, " already exists, skipping")
return 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() var player = player_scene.instantiate()
player.name = str(id) 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) players_container.add_child(player, true)
print("[Peer ", multiplayer.get_unique_id(), "] Player ", id, " spawned at ", player.position)
var nick = Network.players[id]["nick"] # Get player info from Network
player.nickname.text = nick 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) # player.rpc("change_nick", nick)
# Set up HUD for local player # Set up HUD for local player
@@ -356,6 +473,14 @@ func remove_world_weapon(weapon_id: int):
print("[ERROR] remove_world_weapon called on client!") print("[ERROR] remove_world_weapon called on client!")
return 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 # Broadcast removal to all clients
print("[Server] Broadcasting removal RPC to all clients") print("[Server] Broadcasting removal RPC to all clients")
rpc("_remove_weapon_on_clients", weapon_id) rpc("_remove_weapon_on_clients", weapon_id)
+8 -3
View File
@@ -16,7 +16,7 @@ func animate(_velocity: Vector3) -> void:
return return
# Don't override attack animation if it's playing # 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 return
# Check if we're dashing # Check if we're dashing
@@ -54,9 +54,14 @@ func _play_animation(anim_name: String):
animation_player.play(anim_name) animation_player.play(anim_name)
# Silently ignore if animation doesn't exist # Silently ignore if animation doesn't exist
func play_attack() -> void: func play_attack(anim_name: String = "Attack_OneHand") -> void:
if animation_player: if animation_player:
# Play attack animation once (don't loop) # Play attack animation once (don't loop)
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.play("Attack_OneHand", -1, 1.0)
# Ensure it doesn't loop
animation_player.animation_set_next("Attack_OneHand", "") animation_player.animation_set_next("Attack_OneHand", "")
-4
View File
@@ -13,10 +13,6 @@ var player_info = {
signal player_connected(peer_id, player_info) signal player_connected(peer_id, player_info)
signal server_disconnected signal server_disconnected
func _process(_delta):
if Input.is_action_just_pressed("quit"):
get_tree().quit(0)
func _ready() -> void: func _ready() -> void:
multiplayer.server_disconnected.connect(_on_connection_failed) multiplayer.server_disconnected.connect(_on_connection_failed)
multiplayer.connection_failed.connect(_on_server_disconnected) multiplayer.connection_failed.connect(_on_server_disconnected)
+111 -44
View File
@@ -42,7 +42,12 @@ var is_blocking: bool = false
@export var attack_damage: float = 10.0 @export var attack_damage: float = 10.0
@export var attack_range: float = 3.0 @export var attack_range: float = 3.0
@export var attack_cooldown: float = 0.5 @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 _attack_timer: float = 0.0
var _unarmed_hitbox: HitBox = null
# Dash system # Dash system
@export var dash_speed_multiplier: float = 2.0 @export var dash_speed_multiplier: float = 2.0
@@ -64,7 +69,10 @@ func _enter_tree():
func _ready(): func _ready():
super._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 # Capture mouse for local player
if is_multiplayer_authority(): if is_multiplayer_authority():
@@ -72,13 +80,18 @@ func _ready():
# Auto-find body node (needed for instanced scenes where @export NodePath doesn't work reliably) # Auto-find body node (needed for instanced scenes where @export NodePath doesn't work reliably)
if _body == null: if _body == null:
for child in get_children(): # Try specific paths first
if child.name == "Armature" or child.name == "3DGodotRobot": if has_node("LilguyRigged/Armature"):
_body = child _body = get_node("LilguyRigged/Armature")
print("Auto-found _body: ", child.name) print("Auto-found _body: LilguyRigged/Armature")
break elif has_node("Armature"):
if _body == null: _body = get_node("Armature")
push_error("Could not find body node (Armature or 3DGodotRobot)!") 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 # Auto-find spring arm offset
if _spring_arm_offset == null: if _spring_arm_offset == null:
@@ -144,6 +157,9 @@ func _ready():
# Setup weapon pickup detection area # Setup weapon pickup detection area
_setup_weapon_pickup_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 # Auto-find weapon attachment if not set
if _weapon_attachment == null: if _weapon_attachment == null:
var bone_attach = get_node_or_null("3DGodotRobot/RobotArmature/Skeleton3D/BoneAttachment3D") var bone_attach = get_node_or_null("3DGodotRobot/RobotArmature/Skeleton3D/BoneAttachment3D")
@@ -189,18 +205,18 @@ func _physics_process(delta):
freeze() freeze()
return return
# Apply gravity when not on floor
if not is_on_floor(): if not is_on_floor():
velocity.y -= gravity * delta velocity.y -= gravity * delta
_body.animate(velocity)
if is_on_floor(): # Handle jump
if Input.is_action_just_pressed("jump"): if is_on_floor() and Input.is_action_just_pressed("jump"):
velocity.y = JUMP_VELOCITY velocity.y = JUMP_VELOCITY
else:
velocity.y -= gravity * delta
_move() _move()
move_and_slide() move_and_slide()
if _body:
_body.animate(velocity) _body.animate(velocity)
func _process(delta): func _process(delta):
@@ -269,6 +285,7 @@ func freeze():
velocity.x = 0 velocity.x = 0
velocity.z = 0 velocity.z = 0
_current_speed = 0 _current_speed = 0
if _body:
_body.animate(Vector3.ZERO) _body.animate(Vector3.ZERO)
func _move() -> void: func _move() -> void:
@@ -276,6 +293,7 @@ func _move() -> void:
if _is_dashing: if _is_dashing:
velocity.x = _dash_direction.x * _current_speed * dash_speed_multiplier velocity.x = _dash_direction.x * _current_speed * dash_speed_multiplier
velocity.z = _dash_direction.z * _current_speed * dash_speed_multiplier velocity.z = _dash_direction.z * _current_speed * dash_speed_multiplier
if _body:
_body.apply_rotation(velocity) _body.apply_rotation(velocity)
return return
@@ -289,11 +307,13 @@ func _move() -> void:
var _direction: Vector3 = transform.basis * Vector3(_input_direction.x, 0, _input_direction.y).normalized() var _direction: Vector3 = transform.basis * Vector3(_input_direction.x, 0, _input_direction.y).normalized()
is_running() is_running()
if _spring_arm_offset:
_direction = _direction.rotated(Vector3.UP, _spring_arm_offset.rotation.y) _direction = _direction.rotated(Vector3.UP, _spring_arm_offset.rotation.y)
if _direction: if _direction:
velocity.x = _direction.x * _current_speed velocity.x = _direction.x * _current_speed
velocity.z = _direction.z * _current_speed velocity.z = _direction.z * _current_speed
if _body:
_body.apply_rotation(velocity) _body.apply_rotation(velocity)
return return
@@ -392,7 +412,7 @@ func _perform_attack():
# Fallback to default unarmed attack # Fallback to default unarmed attack
# Don't attack if already attacking # 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 return
if _attack_timer > 0: if _attack_timer > 0:
@@ -402,31 +422,12 @@ func _perform_attack():
# Play attack animation once # Play attack animation once
if _body: 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 # Activate unarmed hitbox for damage detection
var space_state = get_world_3d().direct_space_state _activate_unarmed_hitbox()
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
## Server-side damage application ## Server-side damage application
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
@@ -462,11 +463,13 @@ func _on_health_changed(_old_health: float, _new_health: float):
_update_health_display() _update_health_display()
func _on_died(killer_id: int): func _on_died(killer_id: int):
# Disable player when dead 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_physics_process(false)
set_process(false) set_process(false)
# Visual feedback - could add death animation here # Visual feedback - runs on all peers
if _body: if _body:
_body.visible = false _body.visible = false
@@ -483,18 +486,19 @@ func _on_died(killer_id: int):
get_node("HealthUI/HealthText").text = "DEAD - Respawning..." get_node("HealthUI/HealthText").text = "DEAD - Respawning..."
func _on_respawned(): func _on_respawned():
# Re-enable player 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_physics_process(true)
set_process(true) set_process(true)
print("[Player ", name, "] Re-enabled physics processing")
# Visual feedback - runs on all peers
if _body: if _body:
_body.visible = true _body.visible = true
_update_health_display() _update_health_display()
if is_multiplayer_authority():
print("You respawned!")
## Dash system ## Dash system
func _perform_dash(): func _perform_dash():
if not is_multiplayer_authority() or is_dead or not is_on_floor(): if not is_multiplayer_authority() or is_dead or not is_on_floor():
@@ -524,6 +528,12 @@ func _perform_dash():
# Animation is handled by the Body's animate function (Jump animation plays during 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 ## Override hurt animation from BaseUnit
func _play_hurt_animation(): func _play_hurt_animation():
if _body and _body.animation_player: if _body and _body.animation_player:
@@ -577,6 +587,63 @@ func _setup_weapon_pickup_area():
pickup_area.area_entered.connect(_on_weapon_area_entered) pickup_area.area_entered.connect(_on_weapon_area_entered)
pickup_area.area_exited.connect(_on_weapon_area_exited) 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:
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):
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()
func _on_weapon_area_entered(area: Area3D): func _on_weapon_area_entered(area: Area3D):
# Check if the area belongs to a WorldWeapon # Check if the area belongs to a WorldWeapon
var weapon = area.get_parent() var weapon = area.get_parent()
+8
View File
@@ -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 attack_animation: String = "Attack1" # Animation to play when attacking
@export var knockback_force: float = 8.0 # How much to push the target back @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_category("Defense Stats")
@export var can_block: bool = false @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%) @export_range(0.0, 1.0) var block_reduction: float = 0.5 # Percentage of damage blocked (0.5 = 50%)
+2
View File
@@ -100,3 +100,5 @@ toggle_character_sheet={
3d_physics/layer_1="player" 3d_physics/layer_1="player"
3d_physics/layer_2="world" 3d_physics/layer_2="world"
3d_physics/layer_3="weapon" 3d_physics/layer_3="weapon"
3d_physics/layer_4="hitbox"
3d_physics/layer_5="hurtbox"