delete files in nickes

This commit is contained in:
2026-01-11 03:34:14 +01:00
parent b692b4f39d
commit 9b55af2e67
46 changed files with 2237 additions and 654 deletions

View File

@@ -1,19 +0,0 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://bpxm3p5gfrgm1"
path="res://.godot/imported/gate_down_01.wav.mp3-06b671985a213cda6f5a35bcc781a782.mp3str"
[deps]
source_file="res://assets/audio/sfx/nickes/gate/gate_down_01.wav.mp3"
dest_files=["res://.godot/imported/gate_down_01.wav.mp3-06b671985a213cda6f5a35bcc781a782.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

View File

@@ -1,19 +0,0 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://b7yj6biew1j26"
path="res://.godot/imported/gate_up_01.wav.mp3-cc95043b481c4ac80f7769caed655e99.mp3str"
[deps]
source_file="res://assets/audio/sfx/nickes/gate/gate_up_01.wav.mp3"
dest_files=["res://.godot/imported/gate_up_01.wav.mp3-cc95043b481c4ac80f7769caed655e99.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

View File

@@ -1,19 +0,0 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://dvlqqeg7pdfbl"
path="res://.godot/imported/sword-impact-metalgate-01.wav.mp3-d49036fcc047a1cffb123a7e098d3d38.mp3str"
[deps]
source_file="res://assets/audio/sfx/nickes/gate/sword-impact-metalgate-01.wav.mp3"
dest_files=["res://.godot/imported/sword-impact-metalgate-01.wav.mp3-d49036fcc047a1cffb123a7e098d3d38.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

View File

@@ -1,19 +0,0 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://bmlnb8rxnf1pc"
path="res://.godot/imported/sword-impact-metalgate-02.wav.mp3-93ab283b4523b09e36ff573d44c052fa.mp3str"
[deps]
source_file="res://assets/audio/sfx/nickes/gate/sword-impact-metalgate-02.wav.mp3"
dest_files=["res://.godot/imported/sword-impact-metalgate-02.wav.mp3-93ab283b4523b09e36ff573d44c052fa.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

View File

@@ -1,19 +0,0 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://d35fucpoaeubv"
path="res://.godot/imported/move_rock.mp3.wav.mp3-38dcfc2a649fce8226fd0ad8768af74e.mp3str"
[deps]
source_file="res://assets/audio/sfx/nickes/move_rock/move_rock.mp3.wav.mp3"
dest_files=["res://.godot/imported/move_rock.mp3.wav.mp3-38dcfc2a649fce8226fd0ad8768af74e.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

View File

@@ -1,19 +0,0 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://c0s6ffsj6i0lq"
path="res://.godot/imported/rock_push_loop_01.mp3-969051a8fa1daf698c0d71cbba84db78.mp3str"
[deps]
source_file="res://assets/audio/sfx/nickes/move_rock/rock_push_loop_01.mp3"
dest_files=["res://.godot/imported/rock_push_loop_01.mp3-969051a8fa1daf698c0d71cbba84db78.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

View File

@@ -1,19 +0,0 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://dsokwxmutlwk5"
path="res://.godot/imported/rock_push_loop_02.mp3-adb5a02227fd633528c7173c1e8ad8d5.mp3str"
[deps]
source_file="res://assets/audio/sfx/nickes/move_rock/rock_push_loop_02.mp3"
dest_files=["res://.godot/imported/rock_push_loop_02.mp3-adb5a02227fd633528c7173c1e8ad8d5.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

View File

@@ -1,19 +0,0 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://b7ye0jis0bsjt"
path="res://.godot/imported/coin_drop_01.wav.mp3-3c6099bab02543ce9ed512438c65dec2.mp3str"
[deps]
source_file="res://assets/audio/sfx/nickes/treasure/coin_drop_01.wav.mp3"
dest_files=["res://.godot/imported/coin_drop_01.wav.mp3-3c6099bab02543ce9ed512438c65dec2.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

View File

@@ -1,19 +0,0 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://dmeqevnmlo24r"
path="res://.godot/imported/coin_drop_02.wav.mp3-1892ea743aa7a945a895b9ead4db5da0.mp3str"
[deps]
source_file="res://assets/audio/sfx/nickes/treasure/coin_drop_02.wav.mp3"
dest_files=["res://.godot/imported/coin_drop_02.wav.mp3-1892ea743aa7a945a895b9ead4db5da0.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

View File

@@ -1,19 +0,0 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://dw7nvjsfoc4vw"
path="res://.godot/imported/open_chest_01.wav.mp3-763d26fb2e11ab8cfcd1a54db1ee14da.mp3str"
[deps]
source_file="res://assets/audio/sfx/nickes/treasure/open_chest_01.wav.mp3"
dest_files=["res://.godot/imported/open_chest_01.wav.mp3-763d26fb2e11ab8cfcd1a54db1ee14da.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

View File

@@ -1,19 +0,0 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://cu1eavghp85pg"
path="res://.godot/imported/open_chest_02.wav.mp3-505d7b4d1fb554615cbbfd5d00736eb7.mp3str"
[deps]
source_file="res://assets/audio/sfx/nickes/treasure/open_chest_02.wav.mp3"
dest_files=["res://.godot/imported/open_chest_02.wav.mp3-505d7b4d1fb554615cbbfd5d00736eb7.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

View File

@@ -1,19 +0,0 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://le2kwiriyjxo"
path="res://.godot/imported/open_treasure_01.wav.mp3-6ef47f3905258255ed532f3287454f2a.mp3str"
[deps]
source_file="res://assets/audio/sfx/nickes/treasure/open_treasure_01.wav.mp3"
dest_files=["res://.godot/imported/open_treasure_01.wav.mp3-6ef47f3905258255ed532f3287454f2a.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

View File

@@ -1,6 +1,7 @@
[gd_scene format=3 uid="uid://d24xrw86pfg1s"] [gd_scene format=3 uid="uid://d24xrw86pfg1s"]
[ext_resource type="Script" uid="uid://b4wejvn0dfrji" path="res://scripts/teleporter_into_closed_room.gd" id="1_g3s7f"] [ext_resource type="Script" uid="uid://b4wejvn0dfrji" path="res://scripts/teleporter_into_closed_room.gd" id="1_g3s7f"]
[ext_resource type="AudioStream" uid="uid://digavvapakqaw" path="res://assets/audio/sfx/teleport.ogg" id="2_7twcj"]
[sub_resource type="Gradient" id="Gradient_skeae"] [sub_resource type="Gradient" id="Gradient_skeae"]
offsets = PackedFloat32Array(0, 0.9883721) offsets = PackedFloat32Array(0, 0.9883721)
@@ -97,18 +98,23 @@ randomness = 1.0
process_material = SubResource("ParticleProcessMaterial_n1yim") process_material = SubResource("ParticleProcessMaterial_n1yim")
[node name="AreaWhichTeleportsPlayerIntoRoom" type="Area2D" parent="." unique_id=47060921] [node name="AreaWhichTeleportsPlayerIntoRoom" type="Area2D" parent="." unique_id=47060921]
collision_mask = 0 collision_layer = 0
[node name="CollisionShape2D" type="CollisionShape2D" parent="AreaWhichTeleportsPlayerIntoRoom" unique_id=1803123867] [node name="CollisionShape2D" type="CollisionShape2D" parent="AreaWhichTeleportsPlayerIntoRoom" unique_id=1803123867]
shape = SubResource("RectangleShape2D_pp12y") shape = SubResource("RectangleShape2D_pp12y")
[node name="AreaToStartEmit" type="Area2D" parent="." unique_id=1219098269] [node name="AreaToStartEmit" type="Area2D" parent="." unique_id=1219098269]
collision_mask = 0 collision_layer = 0
[node name="CollisionShape2D" type="CollisionShape2D" parent="AreaToStartEmit" unique_id=700191159] [node name="CollisionShape2D" type="CollisionShape2D" parent="AreaToStartEmit" unique_id=700191159]
shape = SubResource("RectangleShape2D_7twcj") shape = SubResource("RectangleShape2D_7twcj")
debug_color = Color(0.6530463, 0.21585448, 0.70196074, 0.41960785) debug_color = Color(0.6530463, 0.21585448, 0.70196074, 0.41960785)
[node name="TeleportSfx" type="AudioStreamPlayer2D" parent="." unique_id=1891023570]
stream = ExtResource("2_7twcj")
attenuation = 7.999997
panning_strength = 1.15
[connection signal="body_entered" from="AreaWhichTeleportsPlayerIntoRoom" to="." method="_on_area_which_teleports_player_into_room_body_entered"] [connection signal="body_entered" from="AreaWhichTeleportsPlayerIntoRoom" to="." method="_on_area_which_teleports_player_into_room_body_entered"]
[connection signal="body_entered" from="AreaToStartEmit" to="." method="_on_area_to_start_emit_body_entered"] [connection signal="body_entered" from="AreaToStartEmit" to="." method="_on_area_to_start_emit_body_entered"]
[connection signal="body_exited" from="AreaToStartEmit" to="." method="_on_area_to_start_emit_body_exited"] [connection signal="body_exited" from="AreaToStartEmit" to="." method="_on_area_to_start_emit_body_exited"]

View File

@@ -5,13 +5,22 @@
[ext_resource type="PackedScene" uid="uid://d24xrw86pfg1s" path="res://scenes/TeleporterIntoClosedRoom.tscn" id="2_q5w8r"] [ext_resource type="PackedScene" uid="uid://d24xrw86pfg1s" path="res://scenes/TeleporterIntoClosedRoom.tscn" id="2_q5w8r"]
[ext_resource type="AudioStream" uid="uid://dfolu80c534j4" path="res://assets/audio/sfx/environment/keydoor/unlock.mp3" id="3_la1wf"] [ext_resource type="AudioStream" uid="uid://dfolu80c534j4" path="res://assets/audio/sfx/environment/keydoor/unlock.mp3" id="3_la1wf"]
[ext_resource type="AudioStream" uid="uid://2w73l4k3704x" path="res://assets/audio/sfx/environment/pot/pot_drag1.mp3" id="4_18pbm"] [ext_resource type="AudioStream" uid="uid://2w73l4k3704x" path="res://assets/audio/sfx/environment/pot/pot_drag1.mp3" id="4_18pbm"]
[ext_resource type="AudioStream" uid="uid://c6bp156a5ggdf" path="res://assets/audio/sfx/environment/keydoor/door_closes.mp3" id="5_18pbm"] [ext_resource type="AudioStream" uid="uid://b7yj6biew1j26" path="res://assets/audio/sfx/environment/gate/gate_up_01.wav.mp3" id="6_ju5n0"]
[ext_resource type="AudioStream" uid="uid://dsokwxmutlwk5" path="res://assets/audio/sfx/environment/move_rock/rock_push_loop_02.mp3" id="7_kgbum"]
[ext_resource type="AudioStream" uid="uid://c0s6ffsj6i0lq" path="res://assets/audio/sfx/environment/move_rock/rock_push_loop_01.mp3" id="7_pg2b6"]
[ext_resource type="AudioStream" uid="uid://bpxm3p5gfrgm1" path="res://assets/audio/sfx/environment/gate/gate_down_01.wav.mp3" id="8_pg2b6"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_uvdjg"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_uvdjg"]
size = Vector2(26, 14) size = Vector2(26, 16)
[sub_resource type="RectangleShape2D" id="RectangleShape2D_la1wf"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_la1wf"]
size = Vector2(22, 18) size = Vector2(22, 20)
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_ey00f"]
playback_mode = 1
streams_count = 2
stream_0/stream = ExtResource("7_pg2b6")
stream_1/stream = ExtResource("7_kgbum")
[node name="Door" type="StaticBody2D" unique_id=371155975] [node name="Door" type="StaticBody2D" unique_id=371155975]
collision_layer = 64 collision_layer = 64
@@ -21,27 +30,45 @@ script = ExtResource("1_uvdjg")
position = Vector2(0, -16) position = Vector2(0, -16)
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1520856168] [node name="Sprite2D" type="Sprite2D" parent="." unique_id=1520856168]
position = Vector2(0, -1)
texture = ExtResource("1_hpvv5") texture = ExtResource("1_hpvv5")
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=1691515105] [node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=1691515105]
shape = SubResource("RectangleShape2D_uvdjg") shape = SubResource("RectangleShape2D_uvdjg")
metadata/_edit_lock_ = true
[node name="SfxOpenKeyDoor" type="AudioStreamPlayer2D" parent="." unique_id=47303726] [node name="SfxOpenKeyDoor" type="AudioStreamPlayer2D" parent="." unique_id=47303726]
stream = ExtResource("3_la1wf") stream = ExtResource("3_la1wf")
max_distance = 1485.0
attenuation = 6.276666
panning_strength = 1.09
[node name="SfxOpenStoneDoor" type="AudioStreamPlayer2D" parent="." unique_id=885417421] [node name="SfxOpenStoneDoor" type="AudioStreamPlayer2D" parent="." unique_id=885417421]
stream = ExtResource("4_18pbm") stream = ExtResource("4_18pbm")
max_distance = 1204.0
attenuation = 6.498014
panning_strength = 1.25
[node name="SfxOpenGateDoor" type="AudioStreamPlayer2D" parent="." unique_id=442358170] [node name="SfxOpenGateDoor" type="AudioStreamPlayer2D" parent="." unique_id=442358170]
stream = ExtResource("4_18pbm") stream = ExtResource("6_ju5n0")
volume_db = -4.65
pitch_scale = 1.1
max_distance = 1246.0
attenuation = 7.999997
panning_strength = 1.3
[node name="KeyInteractionArea" type="Area2D" parent="." unique_id=982067740] [node name="KeyInteractionArea" type="Area2D" parent="." unique_id=982067740]
metadata/_edit_lock_ = true
[node name="CollisionShape2D" type="CollisionShape2D" parent="KeyInteractionArea" unique_id=1640987231] [node name="CollisionShape2D" type="CollisionShape2D" parent="KeyInteractionArea" unique_id=1640987231]
shape = SubResource("RectangleShape2D_la1wf") shape = SubResource("RectangleShape2D_la1wf")
debug_color = Color(0.70196074, 0.67558956, 0.17869899, 0.41960785) debug_color = Color(0.70196074, 0.67558956, 0.17869899, 0.41960785)
[node name="SfxDoorCloses" type="AudioStreamPlayer2D" parent="." unique_id=1074871158] [node name="SfxDoorCloses" type="AudioStreamPlayer2D" parent="." unique_id=1074871158]
stream = ExtResource("5_18pbm") stream = SubResource("AudioStreamRandomizer_ey00f")
max_distance = 1333.0 volume_db = -1.268
attenuation = 5.8563395 max_distance = 1289.0
attenuation = 3.7321312
[node name="SfxCloseGateDoor" type="AudioStreamPlayer2D" parent="." unique_id=1825261269]
stream = ExtResource("8_pg2b6")

View File

@@ -20,6 +20,13 @@
[ext_resource type="AudioStream" uid="uid://cg1ndvx4t7xtd" path="res://assets/audio/sfx/environment/pot/pot_destroy_sound5.mp3" id="9_r4pxp"] [ext_resource type="AudioStream" uid="uid://cg1ndvx4t7xtd" path="res://assets/audio/sfx/environment/pot/pot_destroy_sound5.mp3" id="9_r4pxp"]
[ext_resource type="AudioStream" uid="uid://co7i1f4t8qtqp" path="res://assets/audio/sfx/environment/pot/pot_place_06.mp3" id="9_ylwml"] [ext_resource type="AudioStream" uid="uid://co7i1f4t8qtqp" path="res://assets/audio/sfx/environment/pot/pot_place_06.mp3" id="9_ylwml"]
[ext_resource type="AudioStream" uid="uid://bt5npaenq15h2" path="res://assets/audio/sfx/environment/pot/smaller_pot_crash.mp3" id="10_ygcel"] [ext_resource type="AudioStream" uid="uid://bt5npaenq15h2" path="res://assets/audio/sfx/environment/pot/smaller_pot_crash.mp3" id="10_ygcel"]
[ext_resource type="AudioStream" uid="uid://dw7nvjsfoc4vw" path="res://assets/audio/sfx/environment/treasure/open_chest_01.wav.mp3" id="21_oso25"]
[ext_resource type="AudioStream" uid="uid://cu1eavghp85pg" path="res://assets/audio/sfx/environment/treasure/open_chest_02.wav.mp3" id="22_r4pxp"]
[ext_resource type="AudioStream" uid="uid://le2kwiriyjxo" path="res://assets/audio/sfx/environment/treasure/open_treasure_01.wav.mp3" id="23_ygcel"]
[ext_resource type="AudioStream" uid="uid://c0s6ffsj6i0lq" path="res://assets/audio/sfx/environment/move_rock/rock_push_loop_01.mp3" id="24_ygcel"]
[ext_resource type="AudioStream" uid="uid://dsokwxmutlwk5" path="res://assets/audio/sfx/environment/move_rock/rock_push_loop_02.mp3" id="25_1u1k0"]
[ext_resource type="AudioStream" uid="uid://4ilddgc4lgyq" path="res://assets/audio/sfx/environment/crate/crash_table-04.wav" id="26_vfomk"]
[ext_resource type="AudioStream" uid="uid://c7kc0aw0wevah" path="res://assets/audio/sfx/environment/crate/wood_impact_break.mp3" id="27_2p257"]
[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_nyc8x"] [sub_resource type="CapsuleShape2D" id="CapsuleShape2D_nyc8x"]
radius = 4.0 radius = 4.0
@@ -54,6 +61,25 @@ stream_0/stream = ExtResource("4_gpwir")
stream_1/stream = ExtResource("5_nb01e") stream_1/stream = ExtResource("5_nb01e")
stream_2/stream = ExtResource("6_vfa4w") stream_2/stream = ExtResource("6_vfa4w")
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_vfomk"]
playback_mode = 1
streams_count = 3
stream_0/stream = ExtResource("21_oso25")
stream_1/stream = ExtResource("22_r4pxp")
stream_2/stream = ExtResource("23_ygcel")
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_2p257"]
playback_mode = 1
random_pitch = 1.0059091
streams_count = 2
stream_0/stream = ExtResource("24_ygcel")
stream_1/stream = ExtResource("25_1u1k0")
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_ik3co"]
streams_count = 2
stream_0/stream = ExtResource("26_vfomk")
stream_1/stream = ExtResource("27_2p257")
[node name="InteractableObject" type="CharacterBody2D" unique_id=1472163831] [node name="InteractableObject" type="CharacterBody2D" unique_id=1472163831]
collision_layer = 2 collision_layer = 2
collision_mask = 71 collision_mask = 71
@@ -110,3 +136,14 @@ max_distance = 750.0
attenuation = 10.556063 attenuation = 10.556063
panning_strength = 1.5 panning_strength = 1.5
bus = &"Sfx" bus = &"Sfx"
[node name="SfxOpenChest" type="AudioStreamPlayer2D" parent="." unique_id=743332693]
stream = SubResource("AudioStreamRandomizer_vfomk")
[node name="SfxDragRock" type="AudioStreamPlayer2D" parent="." unique_id=1895903195]
stream = SubResource("AudioStreamRandomizer_2p257")
volume_db = -2.611
[node name="SfxBreakCrate" type="AudioStreamPlayer2D" parent="." unique_id=1799447869]
stream = SubResource("AudioStreamRandomizer_ik3co")
volume_db = -6.092

View File

@@ -7,6 +7,8 @@
[ext_resource type="AudioStream" uid="uid://umoxmryvbm01" path="res://assets/audio/sfx/cloth/leather_cloth_01.wav.mp3" id="5_vl55g"] [ext_resource type="AudioStream" uid="uid://umoxmryvbm01" path="res://assets/audio/sfx/cloth/leather_cloth_01.wav.mp3" id="5_vl55g"]
[ext_resource type="AudioStream" uid="uid://d1qqsganlqnwh" path="res://assets/audio/sfx/pickups/key.mp3" id="6_gyjv8"] [ext_resource type="AudioStream" uid="uid://d1qqsganlqnwh" path="res://assets/audio/sfx/pickups/key.mp3" id="6_gyjv8"]
[ext_resource type="AudioStream" uid="uid://b5xbv7s85sy5o" path="res://assets/audio/sfx/pickups/potion.mp3" id="7_eeo7l"] [ext_resource type="AudioStream" uid="uid://b5xbv7s85sy5o" path="res://assets/audio/sfx/pickups/potion.mp3" id="7_eeo7l"]
[ext_resource type="AudioStream" uid="uid://cnb376ah43nqi" path="res://assets/audio/sfx/pickups/bite-food-01.mp3" id="8_0tqa7"]
[ext_resource type="AudioStream" uid="uid://bbnby1sso3f4v" path="res://assets/audio/sfx/pickups/bite-food-02.mp3" id="9_531sv"]
[sub_resource type="Gradient" id="Gradient_1"] [sub_resource type="Gradient" id="Gradient_1"]
colors = PackedColorArray(0, 0, 0, 1, 0, 0, 0, 0) colors = PackedColorArray(0, 0, 0, 1, 0, 0, 0, 0)
@@ -25,6 +27,13 @@ radius = 3.0
[sub_resource type="CircleShape2D" id="CircleShape2D_1"] [sub_resource type="CircleShape2D" id="CircleShape2D_1"]
radius = 8.0 radius = 8.0
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_37k03"]
playback_mode = 1
random_pitch = 1.0059091
streams_count = 2
stream_0/stream = ExtResource("8_0tqa7")
stream_1/stream = ExtResource("9_531sv")
[node name="Loot" type="CharacterBody2D" unique_id=1373758515] [node name="Loot" type="CharacterBody2D" unique_id=1373758515]
collision_layer = 0 collision_layer = 0
collision_mask = 64 collision_mask = 64
@@ -73,3 +82,6 @@ stream = ExtResource("6_gyjv8")
[node name="SfxPotionCollect" type="AudioStreamPlayer2D" parent="." unique_id=1615824668] [node name="SfxPotionCollect" type="AudioStreamPlayer2D" parent="." unique_id=1615824668]
stream = ExtResource("7_eeo7l") stream = ExtResource("7_eeo7l")
[node name="SfxBananaCollect" type="AudioStreamPlayer2D" parent="." unique_id=1763488179]
stream = SubResource("AudioStreamRandomizer_37k03")

View File

@@ -26,6 +26,8 @@
[ext_resource type="AudioStream" uid="uid://caclaiagfnr2o" path="res://assets/audio/sfx/player/take_damage/player_damaged_05.wav.mp3" id="24_wqfne"] [ext_resource type="AudioStream" uid="uid://caclaiagfnr2o" path="res://assets/audio/sfx/player/take_damage/player_damaged_05.wav.mp3" id="24_wqfne"]
[ext_resource type="AudioStream" uid="uid://dighi525ty7sl" path="res://assets/audio/sfx/player/take_damage/player_damaged_06.wav.mp3" id="25_wnwbv"] [ext_resource type="AudioStream" uid="uid://dighi525ty7sl" path="res://assets/audio/sfx/player/take_damage/player_damaged_06.wav.mp3" id="25_wnwbv"]
[ext_resource type="AudioStream" uid="uid://bdhmel5vyixng" path="res://assets/audio/sfx/player/take_damage/player_damaged_07.wav.mp3" id="26_gl8cc"] [ext_resource type="AudioStream" uid="uid://bdhmel5vyixng" path="res://assets/audio/sfx/player/take_damage/player_damaged_07.wav.mp3" id="26_gl8cc"]
[ext_resource type="AudioStream" uid="uid://4vulahdsj4i2" path="res://assets/audio/sfx/swoosh/throw_01.wav.mp3" id="27_31cv2"]
[ext_resource type="AudioStream" uid="uid://w6yon88kjfml" path="res://assets/audio/sfx/nickes/lift_sfx.ogg" id="28_pf23h"]
[sub_resource type="Gradient" id="Gradient_jej6c"] [sub_resource type="Gradient" id="Gradient_jej6c"]
offsets = PackedFloat32Array(0.7710843, 0.77710843) offsets = PackedFloat32Array(0.7710843, 0.77710843)
@@ -196,3 +198,16 @@ bus = &"Sfx"
stream = SubResource("AudioStreamRandomizer_487ah") stream = SubResource("AudioStreamRandomizer_487ah")
attenuation = 7.7274756 attenuation = 7.7274756
bus = &"Sfx" bus = &"Sfx"
[node name="SfxThrow" type="AudioStreamPlayer2D" parent="." unique_id=961008127]
stream = ExtResource("27_31cv2")
pitch_scale = 0.61
max_distance = 983.0
attenuation = 8.876549
panning_strength = 1.04
[node name="SfxLift" type="AudioStreamPlayer2D" parent="." unique_id=1261167113]
stream = ExtResource("28_pf23h")
max_distance = 1246.0
attenuation = 6.964403
panning_strength = 1.11

View File

@@ -9,7 +9,7 @@ extends StaticBody2D
@export var is_closed: bool = true @export var is_closed: bool = true
var is_closing:bool = false var is_closing:bool = false
var is_opening:bool = false var is_opening:bool = false
var time_to_move:float = 0.5 var time_to_move:float = 0.2
var move_timer:float = 0.0 var move_timer:float = 0.0
var animation_start_position: Vector2 = Vector2.ZERO # Position when animation started var animation_start_position: Vector2 = Vector2.ZERO # Position when animation started
@@ -35,6 +35,9 @@ var connected_switches: Array = [] # Array of floor switch nodes
var requires_enemies: bool = false # True if door requires defeating enemies to open var requires_enemies: bool = false # True if door requires defeating enemies to open
var requires_switch: bool = false # True if door requires activating switches to open var requires_switch: bool = false # True if door requires activating switches to open
# Smoke puff scene for StoneDoor effects
var smoke_puff_scene = preload("res://scenes/smoke_puff.tscn")
# Called when the node enters the scene tree for the first time. # Called when the node enters the scene tree for the first time.
func _ready() -> void: func _ready() -> void:
# Set texture based on door type # Set texture based on door type
@@ -209,6 +212,11 @@ func _process(delta: float) -> void:
# When moved from closed position (open), collision should be DISABLED # When moved from closed position (open), collision should be DISABLED
set_collision_layer_value(7, false) set_collision_layer_value(7, false)
print("Door: Opening animation complete - moved to open position: ", open_position, " (closed: ", closed_position, ", offset: ", open_offset, ") - collision DISABLED") print("Door: Opening animation complete - moved to open position: ", open_position, " (closed: ", closed_position, ", offset: ", open_offset, ") - collision DISABLED")
# Spawn smoke puffs when StoneDoor finishes opening (1-2 puffs)
if type == "StoneDoor":
_spawn_smoke_puffs_on_open()
# Animation finished, reset flags # Animation finished, reset flags
is_opening = false is_opening = false
is_closing = false is_closing = false
@@ -222,6 +230,11 @@ func _process(delta: float) -> void:
# When at closed position, collision should be ENABLED # When at closed position, collision should be ENABLED
set_collision_layer_value(7, true) set_collision_layer_value(7, true)
print("Door: Closing animation complete - moved to closed position: ", closed_position, " - collision ENABLED") print("Door: Closing animation complete - moved to closed position: ", closed_position, " - collision ENABLED")
# Spawn smoke puffs when StoneDoor finishes closing (1-3 puffs)
if type == "StoneDoor":
_spawn_smoke_puffs_on_close()
# Animation finished, reset flags # Animation finished, reset flags
is_opening = false is_opening = false
is_closing = false is_closing = false
@@ -295,6 +308,10 @@ func _update_collision_based_on_position():
set_collision_layer_value(7, false) set_collision_layer_value(7, false)
func _open(): func _open():
# Only open on server/authority in multiplayer, then sync to clients
if multiplayer.has_multiplayer_peer() and not multiplayer.is_server():
return # Clients wait for RPC
$TeleporterIntoClosedRoom.is_enabled = false $TeleporterIntoClosedRoom.is_enabled = false
# CRITICAL: For KeyDoors, ensure they start from closed position before opening # CRITICAL: For KeyDoors, ensure they start from closed position before opening
# KeyDoors should ALWAYS start from closed position when opening (never from open position) # KeyDoors should ALWAYS start from closed position when opening (never from open position)
@@ -338,7 +355,9 @@ func _open():
else: else:
push_error("Door: StoneDoor/GateDoor _open() called but closed_position is zero!") push_error("Door: StoneDoor/GateDoor _open() called but closed_position is zero!")
return return
if type == "GateDoor":
$SfxOpenGateDoor.play()
else:
$SfxOpenStoneDoor.play() $SfxOpenStoneDoor.play()
# CRITICAL: Store starting position for animation (should be closed_position) # CRITICAL: Store starting position for animation (should be closed_position)
@@ -348,7 +367,17 @@ func _open():
is_closing = false is_closing = false
move_timer = 0.0 move_timer = 0.0
# Sync door opening to clients in multiplayer
if multiplayer.has_multiplayer_peer() and multiplayer.is_server() and is_inside_tree():
_sync_door_open.rpc()
# Also sync puzzle_solved state
_sync_puzzle_solved.rpc(puzzle_solved)
func _close(): func _close():
# Only close on server/authority in multiplayer, then sync to clients
if multiplayer.has_multiplayer_peer() and not multiplayer.is_server():
return # Clients wait for RPC
# CRITICAL: KeyDoors should NEVER be closed (they only open with a key and stay open) # CRITICAL: KeyDoors should NEVER be closed (they only open with a key and stay open)
if type == "KeyDoor": if type == "KeyDoor":
print("Door: ERROR - _close() called on KeyDoor! KeyDoors should never be closed!") print("Door: ERROR - _close() called on KeyDoor! KeyDoors should never be closed!")
@@ -401,12 +430,19 @@ func _close():
animation_start_position = position animation_start_position = position
print("Door: Starting close animation from ", animation_start_position, " to ", closed_position, " (offset: ", open_offset, ")") print("Door: Starting close animation from ", animation_start_position, " to ", closed_position, " (offset: ", open_offset, ")")
if type == "GateDoor":
$SfxCloseGateDoor.play()
else:
$SfxDoorCloses.play() $SfxDoorCloses.play()
is_opening = false is_opening = false
is_closing = true is_closing = true
move_timer = 0.0 move_timer = 0.0
$TeleporterIntoClosedRoom.is_enabled = true $TeleporterIntoClosedRoom.is_enabled = true
# Sync door closing to clients in multiplayer
if multiplayer.has_multiplayer_peer() and multiplayer.is_server() and is_inside_tree():
_sync_door_close.rpc()
func _ready_after_setup(): func _ready_after_setup():
# Called after door is fully set up with room references and positioned # Called after door is fully set up with room references and positioned
# NEW LOGIC: Door is positioned at OPEN tile position by game_world # NEW LOGIC: Door is positioned at OPEN tile position by game_world
@@ -646,27 +682,77 @@ func _on_room_exited(body):
# Doors stay in their current state # Doors stay in their current state
func _check_puzzle_state(): func _check_puzzle_state():
# Check if room puzzle is solved # Only check puzzle state on server/authority in multiplayer
# IMPORTANT: Only check puzzle state if we're in the blocking room if multiplayer.has_multiplayer_peer() and not multiplayer.is_server():
if puzzle_solved: return # Clients wait for server to check and sync via RPC
return # Already solved
# CRITICAL: Don't check puzzle state while door is animating (closing or opening)
# This prevents race conditions where switch triggers before door finishes closing
if is_closing or is_opening:
print("Door: Skipping puzzle check - door is animating (is_closing: ", is_closing, ", is_opening: ", is_opening, ")")
return
# Check door's actual state (position-based check is more reliable than flags)
var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0
var is_actually_closed = distance_to_closed < 5.0 # Within 5 pixels of closed position
var is_actually_open = distance_to_closed > 5.0 # More than 5 pixels away from closed position
var collision_enabled = get_collision_layer_value(7) # Check if collision layer 7 is enabled
# CRITICAL: If puzzle_solved is true but door is not actually open (not in open position or collision still enabled),
# allow switch to trigger again to open the door
# This handles race conditions where switch triggers while door is still closing
if puzzle_solved and (not is_actually_open or collision_enabled):
# Door should be open but isn't (position or collision) - reset puzzle_solved to allow switch to trigger again
print("Door: puzzle_solved is true but door is not actually open (position: ", is_actually_open, ", collision: ", collision_enabled, ") - resetting to allow switch to trigger again")
puzzle_solved = false
switches_activated = false
# Check if all enemies are defeated (enemies in blocking room) # Check if all enemies are defeated (enemies in blocking room)
if requires_enemies and _are_all_enemies_defeated(): if requires_enemies and _are_all_enemies_defeated():
print("Door: All enemies defeated! Opening door ", name, " (type: ", type, ", room: ", blocking_room.get("x", "?") if blocking_room and not blocking_room.is_empty() else "?", ",", blocking_room.get("y", "?") if blocking_room and not blocking_room.is_empty() else "?", ")") print("Door: All enemies defeated! Opening door ", name, " (type: ", type, ", room: ", blocking_room.get("x", "?") if blocking_room and not blocking_room.is_empty() else "?", ",", blocking_room.get("y", "?") if blocking_room and not blocking_room.is_empty() else "?", ")")
enemies_defeated = true enemies_defeated = true
puzzle_solved = true puzzle_solved = true
if is_closed: if is_actually_closed:
_open() _open()
return return
# Check if all required switches are activated (switches in switch_room, before the door) # Check if all required switches are activated (switches in switch_room, before the door)
if _are_all_switches_activated(): var all_switches_active = _are_all_switches_activated()
# Check if any connected switches are pillar switches (for special handling)
var has_pillar_switch = false
for switch in connected_switches:
if is_instance_valid(switch) and "switch_type" in switch and switch.switch_type == "pillar":
has_pillar_switch = true
break
if all_switches_active:
# All switches are active - solve puzzle and open door if closed
switches_activated = true switches_activated = true
puzzle_solved = true puzzle_solved = true
if is_closed: # Only open if door is actually closed (not just the flag, but actual position)
# This prevents race condition where switch triggers while door is still closing
if is_actually_closed:
_open() _open()
return return
else:
# Not all switches are active
if puzzle_solved and has_pillar_switch:
# Pillar switch became inactive and door was open - close it and reset puzzle
print("Door: Pillar switch deactivated - closing door ", name)
switches_activated = false
puzzle_solved = false
if not is_actually_closed:
_close()
return
elif puzzle_solved and not has_pillar_switch:
# Walk switch puzzle - once solved, stays solved (door stays open)
# Don't reset puzzle_solved for walk switches
return
else:
# Puzzle not solved yet - just reset flags
switches_activated = false
puzzle_solved = false
func _are_all_enemies_defeated() -> bool: func _are_all_enemies_defeated() -> bool:
# Check if all enemies spawned from spawners in the puzzle room are defeated # Check if all enemies spawned from spawners in the puzzle room are defeated
@@ -693,16 +779,16 @@ func _are_all_enemies_defeated() -> bool:
# Check if enemy is in this room (use position-based check, more reliable) # Check if enemy is in this room (use position-based check, more reliable)
var enemy_in_room = false var enemy_in_room = false
var tile_size = 16 # Use tile_size and room bounds from parent scope (declared below)
var enemy_tile_x = int(child.global_position.x / tile_size) var enemy_tile_x = int(child.global_position.x / 16)
var enemy_tile_y = int(child.global_position.y / tile_size) var enemy_tile_y = int(child.global_position.y / 16)
var room_min_x = target_room.x + 2 var enemy_room_min_x = target_room.x + 2
var room_max_x = target_room.x + target_room.w - 2 var enemy_room_max_x = target_room.x + target_room.w - 2
var room_min_y = target_room.y + 2 var enemy_room_min_y = target_room.y + 2
var room_max_y = target_room.y + target_room.h - 2 var enemy_room_max_y = target_room.y + target_room.h - 2
if enemy_tile_x >= room_min_x and enemy_tile_x < room_max_x and \ if enemy_tile_x >= enemy_room_min_x and enemy_tile_x < enemy_room_max_x and \
enemy_tile_y >= room_min_y and enemy_tile_y < room_max_y: enemy_tile_y >= enemy_room_min_y and enemy_tile_y < enemy_room_max_y:
enemy_in_room = true enemy_in_room = true
# Also check spawner metadata - if enemy has spawner_name matching this room's spawners # Also check spawner metadata - if enemy has spawner_name matching this room's spawners
if child.has_meta("spawner_name"): if child.has_meta("spawner_name"):
@@ -718,9 +804,246 @@ func _are_all_enemies_defeated() -> bool:
# Check if all spawned enemies are dead # Check if all spawned enemies are dead
print("Door: _are_all_enemies_defeated() - Found ", room_spawned_enemies.size(), " spawned enemies in room (", target_room.get("x", "?") if target_room and not target_room.is_empty() else "?", ",", target_room.get("y", "?") if target_room and not target_room.is_empty() else "?", ")") print("Door: _are_all_enemies_defeated() - Found ", room_spawned_enemies.size(), " spawned enemies in room (", target_room.get("x", "?") if target_room and not target_room.is_empty() else "?", ",", target_room.get("y", "?") if target_room and not target_room.is_empty() else "?", ")")
# First, check if any enemies in room_spawned_enemies are still alive
# If any are alive, puzzle is not solved
for enemy in room_spawned_enemies:
if is_instance_valid(enemy):
var enemy_is_dead = false
if "is_dead" in enemy:
enemy_is_dead = enemy.is_dead
else:
enemy_is_dead = enemy.is_queued_for_deletion() or not enemy.is_inside_tree()
if not enemy_is_dead:
print("Door: Enemy ", enemy.name, " is still alive - puzzle not solved yet")
return false # Enemy is still alive, puzzle not solved
# If we have enemies and all are dead, puzzle is solved
if room_spawned_enemies.size() > 0:
print("Door: All ", room_spawned_enemies.size(), " spawned enemies are dead! Puzzle solved!")
return true # All enemies found are dead
# No spawned enemies found - check if spawners have actually spawned enemies before
# CRITICAL: Only consider puzzle solved if spawners have spawned enemies and they're all dead
# Don't solve if spawners haven't spawned yet (e.g., spawn_on_ready=false and player hasn't entered room)
# IMPORTANT: Before checking spawners, verify there are NO ALIVE enemies in the room
# This catches cases where enemies weren't added to room_spawned_enemies due to position check issues
var entities_child = entities_node.get_node_or_null("Entities") if entities_node else null
if not entities_child and entities_node:
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world:
entities_child = game_world.get_node_or_null("Entities")
var tile_size = 16
var room_min_x = target_room.x + 2
var room_max_x = target_room.x + target_room.w - 2
var room_min_y = target_room.y + 2
var room_max_y = target_room.y + target_room.h - 2
if entities_child:
for child in entities_child.get_children():
if child.is_in_group("enemy"):
# Only check enemies that were spawned from spawners
if not child.has_meta("spawned_from_spawner") or not child.get_meta("spawned_from_spawner"):
continue
# Check if enemy is in this room by position
var enemy_tile_x = int(child.global_position.x / tile_size)
var enemy_tile_y = int(child.global_position.y / tile_size)
var enemy_in_room = (enemy_tile_x >= room_min_x and enemy_tile_x < room_max_x and \
enemy_tile_y >= room_min_y and enemy_tile_y < room_max_y)
if not enemy_in_room:
continue # Skip enemies not in this room
# Check if enemy is alive
var enemy_is_alive = false
if "is_dead" in child:
enemy_is_alive = not child.is_dead
else:
enemy_is_alive = not child.is_queued_for_deletion() and child.is_inside_tree()
if enemy_is_alive:
# Found an ALIVE enemy in this room - puzzle not solved!
print("Door: Found ALIVE enemy ", child.name, " in room - puzzle not solved yet (enemy still alive)")
return false
# No alive enemies found in room - now check if spawners have spawned
if room_spawned_enemies.size() == 0: if room_spawned_enemies.size() == 0:
# No spawned enemies found - if door requires enemies, puzzle is not solved # No spawned enemies found - check if spawners have actually spawned enemies before
# But if there were never any enemies, this might mean they haven't spawned yet or all are already dead/removed # CRITICAL: Only consider puzzle solved if spawners have spawned enemies and they're all dead
# Don't solve if spawners haven't spawned yet (e.g., spawn_on_ready=false and player hasn't entered room)
var spawners_in_room = []
var spawners_that_have_spawned = []
# First, check ALL spawners in the room (including destroyed ones by checking for enemies with their names)
for spawner in get_tree().get_nodes_in_group("enemy_spawner"):
if is_instance_valid(spawner):
var spawner_tile_x = int(spawner.global_position.x / 16)
var spawner_tile_y = int(spawner.global_position.y / 16)
if spawner_tile_x >= target_room.x + 2 and spawner_tile_x < target_room.x + target_room.w - 2 and \
spawner_tile_y >= target_room.y + 2 and spawner_tile_y < target_room.y + target_room.h - 2:
spawners_in_room.append(spawner)
# Check if this spawner has spawned by multiple methods:
# 1. Check if spawner has has_ever_spawned flag (most reliable)
# 2. Check if any enemies in scene were spawned by this spawner
# 3. Check if spawner has spawned_enemies list with valid enemies
var has_spawned = false
# First, check if spawner has has_ever_spawned flag
if "has_ever_spawned" in spawner and spawner.has_ever_spawned:
has_spawned = true
# Also check if any enemies in scene were spawned by this spawner (ONLY if they're dead/removed)
# If we find ALIVE enemies, don't mark spawner as spawned - wait until they're dead
if not has_spawned:
var entities_child_for_spawner = entities_node.get_node_or_null("Entities") if entities_node else null
if not entities_child_for_spawner and entities_node:
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world:
entities_child_for_spawner = game_world.get_node_or_null("Entities")
if entities_child_for_spawner:
for child in entities_child_for_spawner.get_children():
if child.is_in_group("enemy"):
if child.has_meta("spawner_name") and child.get_meta("spawner_name") == spawner.name:
# Found an enemy spawned by this spawner
# Check if it's alive - if alive, don't mark spawner as spawned yet
var enemy_is_alive = false
if "is_dead" in child:
enemy_is_alive = not child.is_dead
else:
enemy_is_alive = not child.is_queued_for_deletion() and child.is_inside_tree()
if not enemy_is_alive:
# Enemy is dead - spawner has spawned
has_spawned = true
break
# If enemy is alive, don't mark spawner as spawned - wait until it's dead
# Also check if spawner currently has enemies in its spawned_enemies list
# Only count if all enemies in the list are dead (puzzle can only be solved when all are dead)
if not has_spawned and "spawned_enemies" in spawner:
var spawned_enemies_list = spawner.spawned_enemies
if spawned_enemies_list and spawned_enemies_list.size() > 0:
# Check if all enemies in list are dead
var all_enemies_dead = true
for enemy in spawned_enemies_list:
if is_instance_valid(enemy):
var enemy_is_dead = false
if "is_dead" in enemy:
enemy_is_dead = enemy.is_dead
else:
enemy_is_dead = enemy.is_queued_for_deletion() or not enemy.is_inside_tree()
if not enemy_is_dead:
all_enemies_dead = false
break
# Only mark spawner as spawned if all enemies are dead
if all_enemies_dead:
has_spawned = true
if has_spawned:
spawners_that_have_spawned.append(spawner)
# Also check for spawners that may have been destroyed but had enemies spawned
# Look for any dead enemies with spawner name matching this room AND position in this room
var spawner_name_pattern = "EnemySpawner_" + str(target_room.x) + "_" + str(target_room.y)
var found_dead_enemies_with_matching_spawner = []
if entities_child:
for child in entities_child.get_children():
if child.is_in_group("enemy"):
# Only check enemies that were spawned from spawners
if not child.has_meta("spawned_from_spawner") or not child.get_meta("spawned_from_spawner"):
continue
# Check if enemy is in this room by position
var enemy_tile_x = int(child.global_position.x / tile_size)
var enemy_tile_y = int(child.global_position.y / tile_size)
var enemy_in_room = (enemy_tile_x >= room_min_x and enemy_tile_x < room_max_x and \
enemy_tile_y >= room_min_y and enemy_tile_y < room_max_y)
if not enemy_in_room:
continue # Skip enemies not in this room
# Check if enemy is dead
var enemy_is_dead = false
if "is_dead" in child:
enemy_is_dead = child.is_dead
else:
enemy_is_dead = child.is_queued_for_deletion() or not child.is_inside_tree()
# Only check dead/removed enemies as evidence that spawners spawned
if enemy_is_dead and child.has_meta("spawner_name"):
var enemy_spawner_name = child.get_meta("spawner_name")
if spawner_name_pattern in enemy_spawner_name or enemy_spawner_name == spawner_name_pattern:
found_dead_enemies_with_matching_spawner.append(child)
# Track unique spawner names that have spawned (only based on dead enemies)
var unique_spawner_names_that_spawned = {}
for enemy in found_dead_enemies_with_matching_spawner:
if enemy.has_meta("spawner_name"):
var spawner_name = enemy.get_meta("spawner_name")
unique_spawner_names_that_spawned[spawner_name] = true
# Only mark spawners as spawned if we found DEAD enemies (ensures puzzle only solves when all are dead)
if unique_spawner_names_that_spawned.size() > 0:
# Found dead enemies with matching spawner names - spawners definitely spawned and enemies are dead
if spawners_in_room.size() == 0:
for spawner_name in unique_spawner_names_that_spawned.keys():
spawners_in_room.append(null) # Placeholder for destroyed spawner
spawners_that_have_spawned.append(null) # Count as spawned
print("Door: Spawner ", spawner_name, " was destroyed but spawned enemies that are now all dead - counting as spawned")
else:
# Spawners exist - check if any weren't counted as spawned yet
for spawner_name in unique_spawner_names_that_spawned.keys():
var spawner_already_counted = false
for spawned_spawner in spawners_that_have_spawned:
if is_instance_valid(spawned_spawner) and spawned_spawner.name == spawner_name:
spawner_already_counted = true
break
if not spawner_already_counted:
for i in range(spawners_in_room.size()):
var spawner = spawners_in_room[i]
if is_instance_valid(spawner) and spawner.name == spawner_name:
spawners_that_have_spawned.append(spawner)
print("Door: Found dead enemy from spawner ", spawner_name, " - marking as spawned")
break
# Only solve if:
# 1. There are spawners in the room (or were spawners that spawned)
# 2. All spawners have spawned enemies
# 3. All those enemies are now dead (since no enemies found in room)
# Count valid spawners (including null placeholders for destroyed spawners)
var valid_spawners_count = spawners_in_room.size()
var valid_spawned_count = spawners_that_have_spawned.size()
# If we have spawners (including destroyed ones) and they've all spawned, puzzle is solved
if valid_spawners_count > 0 and valid_spawned_count >= valid_spawners_count:
# All spawners in room have spawned at least once, and no enemies found in room
# This means all spawned enemies are dead - puzzle solved!
print("Door: No spawned enemies found, but all ", valid_spawners_count, " spawners in room have spawned enemies that are all dead - puzzle solved!")
return true
# Also check: if no spawners found (they were destroyed), but this is a puzzle room (has blocking doors),
# and we previously found enemies with matching spawner names that are now gone,
# consider the puzzle solved
if valid_spawners_count == 0 and valid_spawned_count > 0:
# Spawners were destroyed, but we found evidence they spawned
# Since no enemies found, they must all be dead - puzzle solved!
print("Door: No spawners or enemies found, but found evidence of spawned enemies that are now all dead - puzzle solved!")
return true
if valid_spawners_count > 0:
print("Door: Spawners in room (", valid_spawners_count, ") but only ", valid_spawned_count, " have spawned - puzzle not solved yet")
else:
print("Door: No spawned enemies found in room - puzzle not solved yet (enemies may not have spawned or already removed)") print("Door: No spawned enemies found in room - puzzle not solved yet (enemies may not have spawned or already removed)")
return false return false
@@ -743,6 +1066,64 @@ func _are_all_enemies_defeated() -> bool:
print("Door: All ", room_spawned_enemies.size(), " spawned enemies are dead! Puzzle solved!") print("Door: All ", room_spawned_enemies.size(), " spawned enemies are dead! Puzzle solved!")
return true # All enemies are dead return true # All enemies are dead
func _spawn_smoke_puffs_on_close():
# Spawn 1-3 smoke puffs when StoneDoor finishes closing
if not smoke_puff_scene:
return
var puff_count = randi_range(1, 3) # Random between 1-3 puffs
for i in range(puff_count):
var puff = smoke_puff_scene.instantiate()
if puff:
# Spawn at door position with small random offset
var offset_x = randf_range(-8, 8)
var offset_y = randf_range(-8, 8)
puff.global_position = global_position + Vector2(offset_x, offset_y)
puff.z_index = 10 # High z-index to ensure visibility
# Add to Entities node for proper layering
var entities_node = get_tree().get_first_node_in_group("game_world")
if entities_node:
entities_node = entities_node.get_node_or_null("Entities")
if entities_node:
entities_node.add_child(puff)
else:
# Fallback: add to scene root
get_tree().current_scene.add_child(puff)
else:
# Fallback: add to scene root
get_tree().current_scene.add_child(puff)
func _spawn_smoke_puffs_on_open():
# Spawn 1-2 smoke puffs when StoneDoor starts opening
if not smoke_puff_scene:
return
var puff_count = randi_range(1, 2) # Random between 1-2 puffs
for i in range(puff_count):
var puff = smoke_puff_scene.instantiate()
if puff:
# Spawn at door position with small random offset
var offset_x = randf_range(-8, 8)
var offset_y = randf_range(-8, 8)
puff.global_position = global_position + Vector2(offset_x, offset_y)
puff.z_index = 10 # High z-index to ensure visibility
# Add to Entities node for proper layering
var entities_node = get_tree().get_first_node_in_group("game_world")
if entities_node:
entities_node = entities_node.get_node_or_null("Entities")
if entities_node:
entities_node.add_child(puff)
else:
# Fallback: add to scene root
get_tree().current_scene.add_child(puff)
else:
# Fallback: add to scene root
get_tree().current_scene.add_child(puff)
func _are_all_switches_activated() -> bool: func _are_all_switches_activated() -> bool:
# Check if all required switches are activated # Check if all required switches are activated
# CRITICAL: ONLY check connected_switches - switches are explicitly connected when spawned # CRITICAL: ONLY check connected_switches - switches are explicitly connected when spawned
@@ -795,6 +1176,11 @@ func _show_key_indicator():
key_indicator.visible = true key_indicator.visible = true
func teleportPlayer(body: Node2D): func teleportPlayer(body: Node2D):
# Only teleport on server in multiplayer (server is authority for all players)
if multiplayer.has_multiplayer_peer() and not multiplayer.is_server():
# Client should wait for server to sync position
return
var keydoor_open_offset = Vector2.ZERO var keydoor_open_offset = Vector2.ZERO
match direction: match direction:
"Up": "Up":
@@ -805,5 +1191,93 @@ func teleportPlayer(body: Node2D):
keydoor_open_offset = Vector2(16, 0) # Open is 16px LEFT from closed keydoor_open_offset = Vector2(16, 0) # Open is 16px LEFT from closed
"Right": "Right":
keydoor_open_offset = Vector2(-16, 0) # Open is 16px RIGHT from closed keydoor_open_offset = Vector2(-16, 0) # Open is 16px RIGHT from closed
body.position = self.global_position + keydoor_open_offset
var new_position = self.global_position + keydoor_open_offset
if body.is_in_group("player"):
# Player teleportation - set position on server and sync to all clients
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
# Server: set position directly and sync to all clients
body.global_position = new_position
# Also reset velocity to prevent player from moving back
if "velocity" in body:
body.velocity = Vector2.ZERO
# Sync position to all clients (including the teleported player's client)
if body.has_method("_sync_teleport_position") and body.can_send_rpcs and body.is_inside_tree():
body._sync_teleport_position.rpc(new_position)
else:
# Single-player: just set position
body.global_position = new_position
if "velocity" in body:
body.velocity = Vector2.ZERO
else:
# Non-player teleportation (shouldn't happen, but handle it)
body.global_position = new_position
pass pass
@rpc("authority", "reliable")
func _sync_door_open():
# Client receives door open RPC - open the door locally
if multiplayer.has_multiplayer_peer() and not multiplayer.is_server():
# Only open if door is closed (avoid reopening already open doors)
var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0
var is_actually_open = distance_to_closed > 5.0
if not is_actually_open and not is_opening:
# Door is closed - open it
if closed_position != Vector2.ZERO:
position = closed_position
global_position = closed_position
is_closed = true
set_collision_layer_value(7, true)
animation_start_position = position
is_opening = true
is_closing = false
move_timer = 0.0
# Play sound effect on client
if type == "KeyDoor":
$SfxOpenKeyDoor.play()
elif type == "GateDoor":
$SfxOpenGateDoor.play()
else:
$SfxOpenStoneDoor.play()
print("Door: Client received door open RPC for ", name, " - starting open animation")
@rpc("authority", "reliable")
func _sync_puzzle_solved(is_solved: bool):
# Client receives puzzle solved state sync
if multiplayer.has_multiplayer_peer() and not multiplayer.is_server():
puzzle_solved = is_solved
if is_solved:
enemies_defeated = true
switches_activated = true
print("Door: Client received puzzle_solved sync for ", name, " - puzzle_solved: ", is_solved)
@rpc("authority", "reliable")
func _sync_door_close():
# Client receives door close RPC - close the door locally
if multiplayer.has_multiplayer_peer() and not multiplayer.is_server():
# Only close if door is open (avoid reclosing already closed doors)
var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0
var is_actually_at_closed = distance_to_closed < 5.0
if not is_actually_at_closed and not is_closing:
# Door is open - close it
var expected_open_pos = closed_position + open_offset
var distance_to_open = position.distance_to(expected_open_pos)
if distance_to_open > 10.0:
animation_start_position = expected_open_pos
position = expected_open_pos
global_position = expected_open_pos
else:
animation_start_position = position
is_opening = false
is_closing = true
move_timer = 0.0
print("Door: Client received door close RPC for ", name, " - starting close animation")

View File

@@ -289,11 +289,14 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
# 11. Place blocking doors on existing tile doors (after everything else is created) # 11. Place blocking doors on existing tile doors (after everything else is created)
# IMPORTANT: This must happen BEFORE placing enemies, so we know which rooms have monster spawner puzzles # IMPORTANT: This must happen BEFORE placing enemies, so we know which rooms have monster spawner puzzles
var blocking_doors = _place_blocking_doors(all_rooms, all_doors, grid, map_size, rng, start_room_index, exit_room_index) var blocking_doors_result = _place_blocking_doors(all_rooms, all_doors, grid, map_size, rng, start_room_index, exit_room_index)
var blocking_doors = blocking_doors_result.doors if blocking_doors_result.has("doors") else blocking_doors_result
var room_puzzle_data = blocking_doors_result.puzzle_data if blocking_doors_result.has("puzzle_data") else {}
# Extract rooms with monster spawner puzzles (these should NOT have pre-spawned enemies) # Extract rooms with monster spawner puzzles (these should NOT have pre-spawned enemies)
var rooms_with_spawner_puzzles = [] var rooms_with_spawner_puzzles = []
for door_data in blocking_doors: var blocking_doors_array = blocking_doors if blocking_doors is Array else blocking_doors.doors
for door_data in blocking_doors_array:
if "puzzle_type" in door_data and door_data.puzzle_type == "enemy": if "puzzle_type" in door_data and door_data.puzzle_type == "enemy":
if "blocking_room" in door_data and not door_data.blocking_room.is_empty(): if "blocking_room" in door_data and not door_data.blocking_room.is_empty():
var puzzle_room = door_data.blocking_room var puzzle_room = door_data.blocking_room
@@ -333,7 +336,7 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
var room = all_rooms[i] var room = all_rooms[i]
# Skip start room and exit room # Skip start room and exit room
if i != start_room_index and i != exit_room_index: if i != start_room_index and i != exit_room_index:
var room_objects = _place_interactable_objects_in_room(room, grid, map_size, all_doors, all_enemies, rng) var room_objects = _place_interactable_objects_in_room(room, grid, map_size, all_doors, all_enemies, rng, room_puzzle_data)
all_interactable_objects.append_array(room_objects) all_interactable_objects.append_array(room_objects)
# NOTE: Stairs placement was moved earlier (step 7.5, before torches) to prevent torch overlap # NOTE: Stairs placement was moved earlier (step 7.5, before torches) to prevent torch overlap
@@ -550,6 +553,11 @@ func _create_corridor_between_rooms(room1: Dictionary, room2: Dictionary, grid:
grid[x][y] = 2 # Door area (replaces wall) grid[x][y] = 2 # Door area (replaces wall)
# Use door tile coordinates (10,2) + offset for 2x3 door # Use door tile coordinates (10,2) + offset for 2x3 door
tile_grid[x][y] = door_tile_start + Vector2i(door_dx, door_dy) tile_grid[x][y] = door_tile_start + Vector2i(door_dx, door_dy)
if door_dx == 0 and door_dy == 1:
grid[x][y] = 1 # Floor
var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()]
tile_grid[x][y] = floor_tile
# Also create door on LEFT wall of right room (if there's a gap) # Also create door on LEFT wall of right room (if there's a gap)
if corridor_length > 0: if corridor_length > 0:
@@ -564,6 +572,10 @@ func _create_corridor_between_rooms(room1: Dictionary, room2: Dictionary, grid:
grid[x][y] = 2 # Door area (replaces wall) grid[x][y] = 2 # Door area (replaces wall)
# Use door tile coordinates (5,2) + offset for 2x3 door # Use door tile coordinates (5,2) + offset for 2x3 door
tile_grid[x][y] = right_door_tile_start + Vector2i(door_dx, door_dy) tile_grid[x][y] = right_door_tile_start + Vector2i(door_dx, door_dy)
if door_dx == 1 and door_dy == 1:
grid[x][y] = 1 # Floor
var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()]
tile_grid[x][y] = floor_tile
# CRITICAL: room1 = room the door is ON (left room for horizontal doors) # CRITICAL: room1 = room the door is ON (left room for horizontal doors)
# room2 = room the door leads TO (right room for horizontal doors) # room2 = room the door leads TO (right room for horizontal doors)
@@ -633,6 +645,10 @@ func _create_corridor_between_rooms(room1: Dictionary, room2: Dictionary, grid:
grid[x][y] = 2 # Door area (replaces wall) grid[x][y] = 2 # Door area (replaces wall)
# Use door tile coordinates (7,5) + offset for 3x2 door # Use door tile coordinates (7,5) + offset for 3x2 door
tile_grid[x][y] = door_tile_start + Vector2i(door_dx, door_dy) tile_grid[x][y] = door_tile_start + Vector2i(door_dx, door_dy)
if door_dx == 1 and door_dy == 0:
grid[x][y] = 1 # Floor
var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()]
tile_grid[x][y] = floor_tile
# Also create door on TOP wall of bottom room (if there's a gap) # Also create door on TOP wall of bottom room (if there's a gap)
if corridor_length > 0: if corridor_length > 0:
@@ -647,6 +663,10 @@ func _create_corridor_between_rooms(room1: Dictionary, room2: Dictionary, grid:
grid[x][y] = 2 # Door area (replaces wall) grid[x][y] = 2 # Door area (replaces wall)
# Use door tile coordinates (7,0) + offset for 3x2 door # Use door tile coordinates (7,0) + offset for 3x2 door
tile_grid[x][y] = bottom_door_tile_start + Vector2i(door_dx, door_dy) tile_grid[x][y] = bottom_door_tile_start + Vector2i(door_dx, door_dy)
if door_dx == 1 and door_dy == 1:
grid[x][y] = 1 # Floor
var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()]
tile_grid[x][y] = floor_tile
# CRITICAL: room1 = room the door is ON (top room for vertical doors) # CRITICAL: room1 = room the door is ON (top room for vertical doors)
# room2 = room the door leads TO (bottom room for vertical doors) # room2 = room the door leads TO (bottom room for vertical doors)
@@ -744,15 +764,58 @@ func _place_torches_in_room(room: Dictionary, grid: Array, all_doors: Array, _ma
# We need to check all valid Y positions on the left/right walls and place at torch_y_from_floor # We need to check all valid Y positions on the left/right walls and place at torch_y_from_floor
# Left/right walls are valid from room.y + 2 to room.y + room.h - 2 (skipping 2-tile corners) # Left/right walls are valid from room.y + 2 to room.y + room.h - 2 (skipping 2-tile corners)
for y in range(room.y + 2, room.y + room.h - 2): for y in range(room.y + 2, room.y + room.h - 2):
# Check if this is a valid left wall position (use room.x + 1, the right part of the left wall) # Check if this is a valid left wall position
if _is_valid_torch_position(room.x + 1, y, grid, all_doors): # Left wall has 2 tiles: room.x and room.x + 1
# Place at the same distance from floor as top wall torches # Check both tiles to ensure we're not placing on a door
# Use room.x + 1 (the right part of the left wall) for torch placement
if _is_valid_torch_position(room.x, y, grid, all_doors) and _is_valid_torch_position(room.x + 1, y, grid, all_doors):
# Calculate torch world position
# X position is on the left wall: (room.x + 1) * tile_size + tile_size / 2.0 # X position is on the left wall: (room.x + 1) * tile_size + tile_size / 2.0
# Move it further to the left (negative X) to position it better on the wall # Move it further to the left (negative X) to position it better on the wall
var left_wall_x = (room.x + 1) * tile_size + tile_size / 2.0 - 8 # Move 8 pixels to the left var left_wall_x = (room.x + 1) * tile_size + tile_size / 2.0 - 8 # Move 8 pixels to the left
# Y position should be at the same distance from floor: y * tile_size + tile_size / 2.0 + 8 # Y position should be at the same distance from floor: y * tile_size + tile_size / 2.0 + 8
var left_wall_y = y * tile_size + tile_size / 2.0 + torch_y_offset var left_wall_y = y * tile_size + tile_size / 2.0 + torch_y_offset
var world_pos = Vector2(left_wall_x, left_wall_y) var world_pos = Vector2(left_wall_x, left_wall_y)
# CRITICAL: Check if torch's 16x16 pixel bounding box overlaps with any door
# Torch is 16x16 pixels, so it extends 8 pixels in each direction from its center
# Torch bounding box: from (left_wall_x - 8, left_wall_y - 8) to (left_wall_x + 8, left_wall_y + 8)
var torch_bbox_min_x = left_wall_x - 8
var torch_bbox_max_x = left_wall_x + 8
var torch_bbox_min_y = left_wall_y - 8
var torch_bbox_max_y = left_wall_y + 8
# Check if torch bounding box overlaps with any door's bounding box
# Only check doors that are on the left wall of this room (dir="W")
var overlaps_door = false
var tile_size_check = 16
for door in all_doors:
var door_dir = door.dir if "dir" in door else ""
if door_dir != "W": # Only check left doors (dir="W")
continue
# Check if this door is on the left wall of this room
# Left door is on the left wall, so door.x should be room.x (the left tile of the left wall)
var door_x_match = (door.x == room.x)
if not door_x_match:
continue # This door is not on this room's left wall
# Left door (dir="W"): 2 tiles wide, 3 tiles tall
# Door position is at door.x, door.y (upper-left tile)
# Door occupies tiles: x from door.x to door.x + 2, y from door.y to door.y + 3
# Door world bounding box: from (door.x * 16, door.y * 16) to ((door.x + 2) * 16, (door.y + 3) * 16)
var door_min_x = door.x * tile_size_check
var door_max_x = (door.x + 2) * tile_size_check
var door_min_y = door.y * tile_size_check
var door_max_y = (door.y + 3) * tile_size_check
# Check if torch bounding box overlaps with door bounding box
if not (torch_bbox_max_x < door_min_x or torch_bbox_min_x > door_max_x or \
torch_bbox_max_y < door_min_y or torch_bbox_min_y > door_max_y):
overlaps_door = true
break
if not overlaps_door:
valid_wall_positions.append({"pos": world_pos, "wall": "left", "rotation": 270}) valid_wall_positions.append({"pos": world_pos, "wall": "left", "rotation": 270})
break # Only add one torch per wall break # Only add one torch per wall
@@ -761,16 +824,60 @@ func _place_torches_in_room(room: Dictionary, grid: Array, all_doors: Array, _ma
# Right wall has two parts: room.x + room.w - 2 (left part, actual wall) and room.x + room.w - 1 (right part, also corner) # Right wall has two parts: room.x + room.w - 2 (left part, actual wall) and room.x + room.w - 1 (right part, also corner)
# We should place torches on room.x + room.w - 2 (the left part of the 2-tile wide right wall), not on room.x + room.w - 1 (corner) # We should place torches on room.x + room.w - 2 (the left part of the 2-tile wide right wall), not on room.x + room.w - 1 (corner)
# Check all valid Y positions on the right wall # Check all valid Y positions on the right wall
# CRITICAL: Check both tiles of the right wall (similar to left wall) to ensure we're not placing on a door
for y in range(room.y + 2, room.y + room.h - 2): for y in range(room.y + 2, room.y + room.h - 2):
# Check if this is a valid right wall position (use room.x + room.w - 2, the left part of the right wall) # Check if this is a valid right wall position
if _is_valid_torch_position(room.x + room.w - 2, y, grid, all_doors): # Right wall has 2 tiles: room.x + room.w - 2 (left part) and room.x + room.w - 1 (right part/corner)
# Place at the same distance from floor as top wall torches # Check both tiles to ensure we're not placing on a door
# Use room.x + room.w - 2 (the left part of the right wall) for torch placement
if _is_valid_torch_position(room.x + room.w - 2, y, grid, all_doors) and _is_valid_torch_position(room.x + room.w - 1, y, grid, all_doors):
# Calculate torch world position
# X position is on the right wall: (room.x + room.w - 2) * tile_size + tile_size / 2.0 # X position is on the right wall: (room.x + room.w - 2) * tile_size + tile_size / 2.0
# Move it further to the right (positive X) to position it better on the wall # Move it further to the right (positive X) to position it better on the wall
var right_wall_x = (room.x + room.w - 2) * tile_size + tile_size / 2.0 + 8 # Move 8 pixels to the right var right_wall_x = (room.x + room.w - 2) * tile_size + tile_size / 2.0 + 8 # Move 8 pixels to the right
# Y position should be at the same distance from floor: y * tile_size + tile_size / 2.0 + 8 # Y position should be at the same distance from floor: y * tile_size + tile_size / 2.0 + 8
var right_wall_y = y * tile_size + tile_size / 2.0 + torch_y_offset var right_wall_y = y * tile_size + tile_size / 2.0 + torch_y_offset
var world_pos = Vector2(right_wall_x, right_wall_y) var world_pos = Vector2(right_wall_x, right_wall_y)
# CRITICAL: Check if torch's 16x16 pixel bounding box overlaps with any door
# Torch is 16x16 pixels, so it extends 8 pixels in each direction from its center
# Torch bounding box: from (right_wall_x - 8, right_wall_y - 8) to (right_wall_x + 8, right_wall_y + 8)
var torch_bbox_min_x = right_wall_x - 8
var torch_bbox_max_x = right_wall_x + 8
var torch_bbox_min_y = right_wall_y - 8
var torch_bbox_max_y = right_wall_y + 8
# Check if torch bounding box overlaps with any door's bounding box
# Only check doors that are on the right wall of this room (dir="E")
var overlaps_door = false
var tile_size_check = 16
for door in all_doors:
var door_dir = door.dir if "dir" in door else ""
if door_dir != "E": # Only check right doors (dir="E")
continue
# Check if this door is on the right wall of this room
# Right door is on the right wall, so door.x should be room.x + room.w - 2 (the left part of the right wall)
var door_x_match = (door.x == room.x + room.w - 2)
if not door_x_match:
continue # This door is not on this room's right wall
# Right door (dir="E"): 2 tiles wide, 3 tiles tall
# Door position is at door.x, door.y (upper-left tile)
# Door occupies tiles: x from door.x to door.x + 2, y from door.y to door.y + 3
# Door world bounding box: from (door.x * 16, door.y * 16) to ((door.x + 2) * 16, (door.y + 3) * 16)
var door_min_x = door.x * tile_size_check
var door_max_x = (door.x + 2) * tile_size_check
var door_min_y = door.y * tile_size_check
var door_max_y = (door.y + 3) * tile_size_check
# Check if torch bounding box overlaps with door bounding box
if not (torch_bbox_max_x < door_min_x or torch_bbox_min_x > door_max_x or \
torch_bbox_max_y < door_min_y or torch_bbox_min_y > door_max_y):
overlaps_door = true
break
if not overlaps_door:
valid_wall_positions.append({"pos": world_pos, "wall": "right", "rotation": 90}) valid_wall_positions.append({"pos": world_pos, "wall": "right", "rotation": 90})
break # Only add one torch per wall break # Only add one torch per wall
@@ -813,13 +920,25 @@ func _is_valid_torch_position(x: int, y: int, grid: Array, all_doors: Array) ->
for door in all_doors: for door in all_doors:
var door_x = door.x var door_x = door.x
var door_y = door.y var door_y = door.y
var door_w = door.w if "w" in door else 2 # Default door width (2 or 3) var door_dir = door.dir if "dir" in door else ""
var door_h = door.h if "h" in door else 3 # Default door height (2 or 3)
# Calculate actual door dimensions based on direction
# Horizontal doors (E/W): actually 2-3 tiles wide and 3 tiles tall in grid
# Vertical doors (N/S): actually 3 tiles wide and 2-3 tiles tall in grid
var door_w = door.w if "w" in door else 2
var door_h = door.h if "h" in door else 3
var actual_w = door_w
var actual_h = door_h
if door_dir == "E" or door_dir == "W":
# Horizontal door: w is correct (2 or 3), but h is actually 3 in grid (not 1)
actual_h = 3
elif door_dir == "N" or door_dir == "S":
# Vertical door: h is correct (2 or 3), but w is actually 3 in grid (not 1)
actual_w = 3
# Check if (x, y) is within door area # Check if (x, y) is within door area
# For horizontal doors: door.w is width (2 or 3), door.h is 1 if x >= door_x and x < door_x + actual_w and y >= door_y and y < door_y + actual_h:
# For vertical doors: door.w is 1, door.h is height (2 or 3)
if x >= door_x and x < door_x + door_w and y >= door_y and y < door_y + door_h:
return false return false
return true return true
@@ -1161,14 +1280,34 @@ func _place_enemies_in_room(room: Dictionary, grid: Array, map_size: Vector2i, r
else: else:
move_speed = rng.randf_range(50.0, 80.0) # Other enemies (humanoids): faster move_speed = rng.randf_range(50.0, 80.0) # Other enemies (humanoids): faster
enemies.append({ var enemy_data = {
"type": enemy_type, "type": enemy_type,
"position": position, "position": position,
"room": room, # Store reference to room for AI "room": room, # Store reference to room for AI
"max_health": max_health, "max_health": max_health,
"move_speed": move_speed, "move_speed": move_speed,
"damage": damage "damage": damage
}) }
# If it's a humanoid enemy, randomize the humanoid_type
if enemy_type.ends_with("enemy_humanoid.tscn"):
# Random humanoid type: 0=CYCLOPS, 1=DEMON, 2=HUMANOID, 3=NIGHTELF, 4=GOBLIN, 5=ORC, 6=SKELETON
# Weight towards common types (goblins, humans, orcs) - 40% goblin, 30% humanoid, 20% orc, 10% other
var rand_val = rng.randf()
var humanoid_type = 2 # Default to HUMANOID
if rand_val < 0.4:
humanoid_type = 4 # GOBLIN (40%)
elif rand_val < 0.7:
humanoid_type = 2 # HUMANOID (30%)
elif rand_val < 0.9:
humanoid_type = 5 # ORC (20%)
else:
# 10% for other types (distributed evenly)
var other_types = [0, 1, 3, 6] # CYCLOPS, DEMON, NIGHTELF, SKELETON
humanoid_type = other_types[rng.randi() % other_types.size()]
enemy_data["humanoid_type"] = humanoid_type
enemies.append(enemy_data)
return enemies return enemies
@@ -1671,22 +1810,39 @@ func _force_place_stairs(exit_room: Dictionary, grid: Array, tile_grid: Array, m
print("DungeonGenerator: Force placed ", stairs_dir, " stairs at tile (", stairs_data.x, ",", stairs_data.y, ") world pos: ", stairs_data.world_pos) print("DungeonGenerator: Force placed ", stairs_dir, " stairs at tile (", stairs_data.x, ",", stairs_data.y, ") world pos: ", stairs_data.world_pos)
return stairs_data return stairs_data
func _place_interactable_objects_in_room(room: Dictionary, grid: Array, map_size: Vector2i, all_doors: Array, all_enemies: Array, rng: RandomNumberGenerator) -> Array: func _place_interactable_objects_in_room(room: Dictionary, grid: Array, map_size: Vector2i, all_doors: Array, all_enemies: Array, rng: RandomNumberGenerator, room_puzzle_data: Dictionary = {}) -> Array:
# Place interactable objects in a room # Place interactable objects in a room
# Small rooms (7-8 tiles): 0-1 objects # Small rooms (7-8 tiles): 0-1 objects
# Medium rooms (9-10 tiles): 0-3 objects # Medium rooms (9-10 tiles): 0-3 objects
# Large rooms (11-12 tiles): 0-8 objects # Large rooms (11-12 tiles): 0-8 objects
# Returns array of interactable object data dictionaries # Returns array of interactable object data dictionaries
# CRITICAL: If room has "switch_pillar" puzzle, MUST spawn at least 1 Pillar (regardless of room size)
var objects = [] var objects = []
var tile_size = 16 var tile_size = 16
# Check if room has a "switch_pillar" puzzle - if so, we MUST spawn at least 1 pillar
var has_pillar_switch_puzzle = false
if room_puzzle_data.size() > 0:
for puzzle_room in room_puzzle_data.keys():
# Compare rooms by values (x, y, w, h)
if puzzle_room.x == room.x and puzzle_room.y == room.y and \
puzzle_room.w == room.w and puzzle_room.h == room.h:
var puzzle_info = room_puzzle_data[puzzle_room]
print("DungeonGenerator: Checking room (", room.x, ",", room.y, ") - puzzle_room (", puzzle_room.x, ",", puzzle_room.y, ") puzzle_type: ", puzzle_info.type)
if puzzle_info.type == "switch_pillar":
has_pillar_switch_puzzle = true
print("DungeonGenerator: Room (", room.x, ",", room.y, ") has pillar switch puzzle - will spawn at least 1 pillar")
break
else:
print("DungeonGenerator: room_puzzle_data is empty for room (", room.x, ",", room.y, ")")
# Calculate room floor area (excluding walls) # Calculate room floor area (excluding walls)
var floor_w = room.w - 4 # Excluding 2-tile walls on each side var floor_w = room.w - 4 # Excluding 2-tile walls on each side
var floor_h = room.h - 4 var floor_h = room.h - 4
var floor_area = floor_w * floor_h var floor_area = floor_w * floor_h
# Determine max objects based on room size # Determine max objects based on room size
var max_objects: int var max_objects: int = 0
if floor_area <= 16: # Small rooms (4x4 or smaller floor) if floor_area <= 16: # Small rooms (4x4 or smaller floor)
max_objects = 1 max_objects = 1
elif floor_area <= 36: # Medium rooms (up to 6x6 floor) elif floor_area <= 36: # Medium rooms (up to 6x6 floor)
@@ -1696,6 +1852,14 @@ func _place_interactable_objects_in_room(room: Dictionary, grid: Array, map_size
var num_objects = rng.randi_range(0, max_objects) var num_objects = rng.randi_range(0, max_objects)
# CRITICAL: If room has pillar switch puzzle, ensure we spawn at least 1 pillar
# This MUST happen regardless of room size
if has_pillar_switch_puzzle:
# Set minimum to 1 if num_objects is 0
if num_objects == 0:
num_objects = 1
# The pillar will be placed FIRST in the objects list (before any other objects)
# Available object types and their setup functions # Available object types and their setup functions
var object_types = [ var object_types = [
{"type": "Pot", "setup": "setup_pot"}, {"type": "Pot", "setup": "setup_pot"},
@@ -1737,13 +1901,47 @@ func _place_interactable_objects_in_room(room: Dictionary, grid: Array, map_size
if _is_valid_interactable_position(world_pos, all_doors, all_enemies, room): if _is_valid_interactable_position(world_pos, all_doors, all_enemies, room):
valid_positions.append(world_pos) valid_positions.append(world_pos)
# Early return if no valid positions (unless pillar is required, but that's handled below)
if valid_positions.size() == 0: if valid_positions.size() == 0:
if has_pillar_switch_puzzle:
push_warning("DungeonGenerator: Room (", room.x, ",", room.y, ") has pillar switch puzzle but NO valid positions! Cannot place pillar.")
return objects return objects
# Shuffle positions to randomize placement # Shuffle positions to randomize placement
valid_positions.shuffle() valid_positions.shuffle()
# Place objects # CRITICAL: If room has pillar switch puzzle, the FIRST object MUST be a Pillar
if has_pillar_switch_puzzle:
# Place pillar as the first object
var pillar_type_data = {"type": "Pillar", "setup": "setup_pillar"}
objects.append({
"type": pillar_type_data.type,
"setup_function": pillar_type_data.setup,
"position": valid_positions[0],
"room": room
})
# Place remaining objects (skip first position since it's used by the pillar)
# We need to place num_objects - 1 more objects (since pillar counts as 1)
var remaining_objects = num_objects - 1
var positions_index = 1 # Start from second position
for i in range(min(remaining_objects, valid_positions.size() - 1)):
if positions_index >= valid_positions.size():
break # No more valid positions
var object_type_data = object_types[rng.randi() % object_types.size()]
# Skip Pillar type for remaining objects (already placed one)
while object_type_data.type == "Pillar":
object_type_data = object_types[rng.randi() % object_types.size()]
objects.append({
"type": object_type_data.type,
"setup_function": object_type_data.setup,
"position": valid_positions[positions_index],
"room": room
})
positions_index += 1
else:
# Normal placement: no pillar requirement
for i in range(min(num_objects, valid_positions.size())): for i in range(min(num_objects, valid_positions.size())):
var object_type_data = object_types[rng.randi() % object_types.size()] var object_type_data = object_types[rng.randi() % object_types.size()]
var position = valid_positions[i] var position = valid_positions[i]
@@ -1872,7 +2070,7 @@ func _find_rooms_before_door(door: Dictionary, start_room: Dictionary, _all_room
return rooms_before_door return rooms_before_door
func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_size: Vector2i, rng: RandomNumberGenerator, start_room_index: int, exit_room_index: int) -> Array: func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_size: Vector2i, rng: RandomNumberGenerator, start_room_index: int, exit_room_index: int) -> Dictionary:
# Place blocking doors on existing tile doors # Place blocking doors on existing tile doors
# Returns array of blocking door data dictionaries # Returns array of blocking door data dictionaries
var blocking_doors = [] var blocking_doors = []
@@ -1892,6 +2090,7 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
# STEP 1: For each room (except start/exit), randomly decide if it has a door-puzzle # STEP 1: For each room (except start/exit), randomly decide if it has a door-puzzle
var puzzle_room_chance = 0.4 # 40% chance per room var puzzle_room_chance = 0.4 # 40% chance per room
print("DungeonGenerator: Assigning puzzles to rooms (", all_rooms.size(), " total rooms, excluding start/exit)")
for i in range(all_rooms.size()): for i in range(all_rooms.size()):
if i == start_room_index or i == exit_room_index: if i == start_room_index or i == exit_room_index:
continue # Skip start and exit rooms continue # Skip start and exit rooms
@@ -1899,47 +2098,40 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
var room = all_rooms[i] var room = all_rooms[i]
if rng.randf() < puzzle_room_chance: if rng.randf() < puzzle_room_chance:
print("DungeonGenerator: Room (", room.x, ", ", room.y, ") selected for puzzle assignment")
# This room has a puzzle! # This room has a puzzle!
# CRITICAL SAFETY CHECK: Never assign puzzles to start or exit rooms # CRITICAL SAFETY CHECK: Never assign puzzles to start or exit rooms
# Double-check even though we skip them in the loop # Double-check even though we skip them in the loop
if i == start_room_index or i == exit_room_index: if i == start_room_index or i == exit_room_index:
continue continue
# Find all doors that lead OUT OF this room (doors IN this room that exit to other rooms) # Find all doors that are connected to this puzzle room
# These are doors where room1 == this room (doors that start FROM this puzzle room) var doors_in_room = []
var doors_out_of_room = []
for door in all_doors: for door in all_doors:
# CRITICAL: Find doors where room1 == this room (doors that lead OUT OF this room) var door_room1 = door.room1 if ("room1" in door and door.room1 and not door.room1.is_empty()) else null
if not "room1" in door or not door.room1 or door.room1.is_empty(): var door_room2 = door.room2 if ("room2" in door and door.room2 and not door.room2.is_empty()) else null
continue
var door_room1 = door.room1 var room_matches = false
# Compare rooms by position and size (value comparison, not reference)
var door_leads_out_of_this_room = (door_room1.x == room.x and door_room1.y == room.y and \ # Check if room1 matches puzzle room
if door_room1:
room_matches = (door_room1.x == room.x and door_room1.y == room.y and \
door_room1.w == room.w and door_room1.h == room.h) door_room1.w == room.w and door_room1.h == room.h)
if door_leads_out_of_this_room: # Check if room2 matches puzzle room
# CRITICAL: Also check that this door doesn't lead into start or exit room if not room_matches and door_room2:
if not "room2" in door or not door.room2 or door.room2.is_empty(): room_matches = (door_room2.x == room.x and door_room2.y == room.y and \
continue door_room2.w == room.w and door_room2.h == room.h)
var door_room2 = door.room2 # Door is connected to puzzle room
var door_room2_index = -1 if room_matches:
for j in range(all_rooms.size()): doors_in_room.append(door)
var check_room = all_rooms[j]
if check_room.x == door_room2.x and check_room.y == door_room2.y and \
check_room.w == door_room2.w and check_room.h == door_room2.h:
door_room2_index = j
break
# Skip if door leads into start or exit room if doors_in_room.size() == 0:
if door_room2_index == start_room_index or door_room2_index == exit_room_index: print("DungeonGenerator: Room (", room.x, ", ", room.y, ") has no doors connected - skipping puzzle assignment")
continue continue # No doors connected to this room, skip
doors_out_of_room.append(door) print("DungeonGenerator: Room (", room.x, ", ", room.y, ") has ", doors_in_room.size(), " doors - assigning puzzle")
if doors_out_of_room.size() == 0:
continue # No doors leading out of this room, skip
# Decide puzzle type: 33% walk switch, 33% pillar switch, 33% enemy spawner (if room is large enough) # Decide puzzle type: 33% walk switch, 33% pillar switch, 33% enemy spawner (if room is large enough)
var can_have_enemies = false var can_have_enemies = false
@@ -1959,13 +2151,16 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
# Store puzzle data for this room # Store puzzle data for this room
room_puzzle_data[room] = { room_puzzle_data[room] = {
"type": puzzle_type, "type": puzzle_type,
"doors": doors_out_of_room "doors": doors_in_room
} }
print("DungeonGenerator: Stored puzzle data for room (", room.x, ", ", room.y, ") - type: ", puzzle_type, ", doors: ", doors_in_room.size())
# Mark these doors as assigned # Mark these doors as assigned
for door in doors_out_of_room: for door in doors_in_room:
assigned_doors.append(door) assigned_doors.append(door)
print("DungeonGenerator: Assigned puzzles to ", room_puzzle_data.size(), " rooms")
# STEP 2: Create blocking doors for rooms with puzzles # STEP 2: Create blocking doors for rooms with puzzles
# CRITICAL: Blocking doors should ONLY be placed ON THE DOORS IN THE PUZZLE ROOM # CRITICAL: Blocking doors should ONLY be placed ON THE DOORS IN THE PUZZLE ROOM
# NEVER create blocking doors for rooms that are NOT in room_puzzle_data! # NEVER create blocking doors for rooms that are NOT in room_puzzle_data!
@@ -2073,95 +2268,71 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
# For now, create blocking doors for ALL doors in the puzzle room # For now, create blocking doors for ALL doors in the puzzle room
print("DungeonGenerator: Creating blocking doors for room (", room.x, ", ", room.y, ") with ", doors_in_room.size(), " doors, puzzle type: ", puzzle_type, ", puzzle_element type: ", puzzle_element_data.type) print("DungeonGenerator: Creating blocking doors for room (", room.x, ", ", room.y, ") with ", doors_in_room.size(), " doors, puzzle type: ", puzzle_type, ", puzzle_element type: ", puzzle_element_data.type)
for door in doors_in_room: for door in doors_in_room:
# CRITICAL: Verify this door is in the puzzle room (already checked above, but double-check) # Determine direction based on which WALL of the PUZZLE ROOM the door is on
if not "room1" in door or not door.room1 or door.room1.is_empty(): var direction = _determine_door_direction_for_puzzle_room(door, room, all_rooms)
push_error("DungeonGenerator: ERROR - Door in puzzle room (", room.x, ", ", room.y, ") has no room1! Skipping door.")
continue
var door_room1 = door.room1 # CRITICAL: door.x and door.y are the position in room1, not necessarily in puzzle room
# CRITICAL: Verify door.room1 matches the puzzle room EXACTLY (value comparison, not reference) # Need to calculate the correct position on the puzzle room's wall
var door_in_puzzle_room = (door_room1.x == room.x and door_room1.y == room.y and \ var door_room2 = door.room2 if ("room2" in door and door.room2 and not door.room2.is_empty()) else null
door_room1.w == room.w and door_room1.h == room.h)
if not door_in_puzzle_room: # Determine if puzzle room is room2 (if so, door position needs adjustment)
push_error("DungeonGenerator: ERROR - Door room1 (", door_room1.x, ", ", door_room1.y, ") does NOT match puzzle room (", room.x, ", ", room.y, ")! Skipping door.") var puzzle_is_room2 = false
continue # This door is not in the puzzle room, skip - DO NOT CREATE DOOR if door_room2:
puzzle_is_room2 = (door_room2.x == room.x and door_room2.y == room.y and \
door_room2.w == room.w and door_room2.h == room.h)
# CRITICAL: Verify this door is not already assigned to another puzzle room # Calculate door position on the puzzle room's wall
# (This should never happen, but safety check)
if door in assigned_doors:
# Check if this door was assigned to a different room
var already_in_different_room = false
for other_room in room_puzzle_data.keys():
if other_room.x != room.x or other_room.y != room.y:
# This is a different puzzle room - check if door belongs to it
var other_puzzle_info = room_puzzle_data[other_room]
if door in other_puzzle_info.doors:
already_in_different_room = true
break
if already_in_different_room:
push_error("DungeonGenerator: ERROR - Door already assigned to a different puzzle room! Skipping door.")
continue # Door is already in another puzzle room - DO NOT CREATE DOOR HERE
# CRITICAL: Check that this door doesn't lead into start/exit room
if "room2" in door and door.room2 and not door.room2.is_empty():
var door_room2 = door.room2
var door_room2_index = -1
for j in range(all_rooms.size()):
var check_room = all_rooms[j]
if check_room.x == door_room2.x and check_room.y == door_room2.y and \
check_room.w == door_room2.w and check_room.h == door_room2.h:
door_room2_index = j
break
if door_room2_index == start_room_index or door_room2_index == exit_room_index:
print("DungeonGenerator: ERROR - Door leads into start/exit room! Skipping blocking door creation.")
continue
# Determine direction based on door's dir field (E/W/N/S) or calculate from room positions
var direction = ""
if "dir" in door:
# Map door direction to our direction enum
match door.dir:
"E": direction = "Right"
"W": direction = "Left"
"N": direction = "Up"
"S": direction = "Down"
_: direction = _determine_door_direction(door, all_rooms)
else:
direction = _determine_door_direction(door, all_rooms)
# Calculate door position based on new rules:
# Open state positions:
# - UP: tile 2 (row 0, col 2) = door_x+2, door_y+0
# - RIGHT: tile 4 (col 1, row 1) = door_x+1, door_y+1
# - DOWN: tile 5 (row 1, col 1) = door_x+1, door_y+1 (middle column, not rightmost)
# - LEFT: tile 3 (col 1, row 0) = door_x+1, door_y+0
var door_tile_x = door.x var door_tile_x = door.x
var door_tile_y = door.y var door_tile_y = door.y
var open_tile_x = door_tile_x var open_tile_x = door_tile_x
var open_tile_y = door_tile_y var open_tile_y = door_tile_y
# If puzzle room is room2, the door position needs to be moved to the puzzle room's wall
if puzzle_is_room2:
# Door is connecting from room1 to puzzle room (room2)
# We need to calculate the position on puzzle room's wall based on door direction
match direction: match direction:
"Up": "Up":
# Door Up (3x2): Open at tile 2 (row 0, col 2) = door_x+2, door_y+0 # Door on top wall of puzzle room - door.y should be at puzzle_room.y
open_tile_x = door_tile_x + 1 # col 2 (middle column, not rightmost) open_tile_x = door_tile_x # Keep same X (horizontal position)
open_tile_y = door_tile_y + 0 # row 0 (top row) open_tile_y = room.y # Top wall of puzzle room
"Right":
# Door Right (2x3): Open at tile 4 (col 1, row 1) = door_x+1, door_y+1
open_tile_x = door_tile_x + 1 # col 1 (right column)
open_tile_y = door_tile_y + 1 # row 1 (middle row)
"Down": "Down":
# Door Down (3x2): StoneDoor/GateDoor start OPEN at (col 1, row 1) = door_x+1, door_y+1 # Door on bottom wall of puzzle room - door.y should be at puzzle_room.y + room.h - 1
# When entering room, they CLOSE to (col 1, row 0) = door_x+1, door_y+0 (16px up from open) open_tile_x = door_tile_x # Keep same X (horizontal position)
# When solving puzzle, they OPEN back to (col 1, row 1) = door_x+1, door_y+1 open_tile_y = room.y + room.h - 1 # Bottom wall of puzzle room
open_tile_x = door_tile_x + 1 # col 1 (middle column, not rightmost)
open_tile_y = door_tile_y + 1 # row 1 (bottom row - OPEN state, closer to wall)
"Left": "Left":
# Door Left (2x3): Open at tile 3 (col 1, row 0) = door_x+1, door_y+0 # Door on left wall of puzzle room - door.x should be at puzzle_room.x
open_tile_x = door_tile_x + 0 # col 0 (left column) open_tile_x = room.x # Left wall of puzzle room
open_tile_y = door_tile_y + 1 # row 1 (middle row) open_tile_y = door_tile_y # Keep same Y (vertical position)
"Right":
# Door on right wall of puzzle room - door.x should be at puzzle_room.x + room.w - 1
open_tile_x = room.x + room.w - 1 # Right wall of puzzle room
open_tile_y = door_tile_y # Keep same Y (vertical position)
# else: Puzzle room is room1 - door position is already in puzzle room, use as-is
# Adjust position based on door direction and offset from wall
# These offsets are relative to the door's position on the wall
match direction:
"Up":
# Door Up (3x2): Open at middle column, top row
# open_tile_x is already on the wall, adjust to middle column
open_tile_x = open_tile_x + 1 # Middle column (3 tiles wide, so +1 from left edge)
open_tile_y = open_tile_y + 0 # Already at top wall (row 0)
"Right":
# Door Right (2x3): Open at right column, middle row
# open_tile_x is already on the wall, adjust to right column
open_tile_x = open_tile_x + 1 # Right column (already at wall)
open_tile_y = open_tile_y + 1 # Middle row (3 tiles tall, so +1 from top)
"Down":
# Door Down (3x2): Open at middle column, bottom row
# open_tile_x is already on the wall, adjust to middle column
open_tile_x = open_tile_x + 1 # Middle column (3 tiles wide, so +1 from left edge)
open_tile_y = open_tile_y + 1 # Bottom row (2 tiles tall, so +1 from top edge)
"Left":
# Door Left (2x3): Open at left column, middle row
# open_tile_x is already on the wall, adjust to left column
open_tile_x = open_tile_x + 0 # Left column (already at wall)
open_tile_y = open_tile_y + 1 # Middle row (3 tiles tall, so +1 from top)
# Calculate world position from open tile (center of tile) # Calculate world position from open tile (center of tile)
# This is the OPEN position - door will start here and move to CLOSED position when entering room # This is the OPEN position - door will start here and move to CLOSED position when entering room
@@ -2190,18 +2361,10 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
"puzzle_type": puzzle_type # "switch_walk", "switch_pillar", or "enemy" "puzzle_type": puzzle_type # "switch_walk", "switch_pillar", or "enemy"
} }
# CRITICAL: Store room1 and room2 from original door for verification # Store puzzle room as room1 for blocking doors
# Ensure room1 matches blocking_room (puzzle room) door_data.original_room1 = room # Puzzle room is always room1 for blocking doors
if "room1" in door and door.room1:
door_data.original_room1 = door.room1
# CRITICAL: Verify room1 matches puzzle room
if not (door.room1.x == room.x and door.room1.y == room.y and door.room1.w == room.w and door.room1.h == room.h):
push_error("DungeonGenerator: ERROR - door.room1 doesn't match puzzle room! room1: (", door.room1.x, ",", door.room1.y, "), puzzle: (", room.x, ",", room.y, ")")
if "room2" in door and door.room2:
door_data.original_room2 = door.room2
var door_room2_str = "(" + str(door.room2.x) + "," + str(door.room2.y) + ")" if "room2" in door and door.room2 else "(?,?)" print("DungeonGenerator: Creating blocking door for puzzle room (", room.x, ", ", room.y, ") - direction: ", direction, ", open_tile: (", open_tile_x, ",", open_tile_y, ")")
print("DungeonGenerator: Creating blocking door for puzzle room (", room.x, ", ", room.y, ") - door.room1: (", door_room1.x, ",", door_room1.y, "), door.room2: ", door_room2_str, ", direction: ", direction, ", open_tile: (", open_tile_x, ",", open_tile_y, ")")
# CRITICAL: Add puzzle-specific data from the puzzle_element_data created above (shared across all doors in room) # CRITICAL: Add puzzle-specific data from the puzzle_element_data created above (shared across all doors in room)
# Only add door if puzzle element data is valid # Only add door if puzzle element data is valid
@@ -2251,21 +2414,9 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
push_error("DungeonGenerator: ERROR - Door blocking_room (", door_data.blocking_room.x, ",", door_data.blocking_room.y, ") doesn't match puzzle room (", room.x, ",", room.y, ")! This door is for wrong room! SKIPPING DOOR") push_error("DungeonGenerator: ERROR - Door blocking_room (", door_data.blocking_room.x, ",", door_data.blocking_room.y, ") doesn't match puzzle room (", room.x, ",", room.y, ")! This door is for wrong room! SKIPPING DOOR")
continue # Skip this door - it's for the wrong room continue # Skip this door - it's for the wrong room
# FINAL CRITICAL SAFETY CHECK: Verify door.room1 matches puzzle room (door should be IN puzzle room) # Add door to blocking doors list ONLY if it has valid puzzle element
if not "room1" in door or not door.room1 or door.room1.is_empty():
push_error("DungeonGenerator: ERROR - Door has no room1! Cannot verify it's in puzzle room! SKIPPING DOOR")
continue
var final_room1_check = (door.room1.x == room.x and door.room1.y == room.y and \
door.room1.w == room.w and door.room1.h == room.h)
if not final_room1_check:
push_error("DungeonGenerator: ERROR - Door room1 (", door.room1.x, ",", door.room1.y, ") doesn't match puzzle room (", room.x, ",", room.y, ")! This door is NOT in the puzzle room! SKIPPING DOOR")
continue # Skip this door - it's not in the puzzle room
# Add door to blocking doors list ONLY if it has valid puzzle element AND is in correct room
blocking_doors.append(door_data) blocking_doors.append(door_data)
print("DungeonGenerator: Created blocking door for puzzle room (", room.x, ", ", room.y, ") - direction: ", direction, ", open tile: (", open_tile_x, ", ", open_tile_y, "), puzzle_type: ", puzzle_type, ", has_switch: ", door_data.get("requires_switch", false), ", has_enemies: ", door_data.get("requires_enemies", false), ", door.room1: (", door.room1.x, ",", door.room1.y, "), door.room2: (", door.room2.x if "room2" in door and door.room2 else 0, ",", door.room2.y if "room2" in door and door.room2 else 0, ")") print("DungeonGenerator: Created blocking door for puzzle room (", room.x, ", ", room.y, ") - direction: ", direction, ", open tile: (", open_tile_x, ", ", open_tile_y, "), puzzle_type: ", puzzle_type, ", has_switch: ", door_data.get("requires_switch", false), ", has_enemies: ", door_data.get("requires_enemies", false))
# STEP 3: Randomly assign some doors as KeyDoors (except start/exit room doors and already assigned doors) # STEP 3: Randomly assign some doors as KeyDoors (except start/exit room doors and already assigned doors)
var key_door_chance = 0.2 # 20% chance per door var key_door_chance = 0.2 # 20% chance per door
@@ -2385,7 +2536,10 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
blocking_doors.append(door_data) blocking_doors.append(door_data)
return blocking_doors return {
"doors": blocking_doors,
"puzzle_data": room_puzzle_data
}
func _find_floor_switch_position(room: Dictionary, grid: Array, map_size: Vector2i, rng: RandomNumberGenerator, exclude_door_x: int = -1, exclude_door_y: int = -1) -> Dictionary: func _find_floor_switch_position(room: Dictionary, grid: Array, map_size: Vector2i, rng: RandomNumberGenerator, exclude_door_x: int = -1, exclude_door_y: int = -1) -> Dictionary:
# Find a valid floor position for a floor switch in the room # Find a valid floor position for a floor switch in the room
@@ -2429,6 +2583,7 @@ func _find_floor_switch_position(room: Dictionary, grid: Array, map_size: Vector
func _determine_door_direction(door: Dictionary, _all_rooms: Array) -> String: func _determine_door_direction(door: Dictionary, _all_rooms: Array) -> String:
# Determine door direction based on door position and connected rooms # Determine door direction based on door position and connected rooms
# Door on upper wall = "Up", left wall = "Left", etc. # Door on upper wall = "Up", left wall = "Left", etc.
# This returns direction relative to room1
if not "room1" in door or not "room2" in door: if not "room1" in door or not "room2" in door:
return "Up" # Default return "Up" # Default
@@ -2436,10 +2591,10 @@ func _determine_door_direction(door: Dictionary, _all_rooms: Array) -> String:
var room2 = door.room2 var room2 = door.room2
# Determine which wall the door is on by comparing room positions # Determine which wall the door is on by comparing room positions
# If room2 is above room1, door is on top wall (Up) # If room2 is above room1, door is on top wall of room1 (Up)
# If room2 is below room1, door is on bottom wall (Down) # If room2 is below room1, door is on bottom wall of room1 (Down)
# If room2 is left of room1, door is on left wall (Left) # If room2 is left of room1, door is on left wall of room1 (Left)
# If room2 is right of room1, door is on right wall (Right) # If room2 is right of room1, door is on right wall of room1 (Right)
var dx = room2.x - room1.x var dx = room2.x - room1.x
var dy = room2.y - room1.y var dy = room2.y - room1.y
@@ -2448,12 +2603,73 @@ func _determine_door_direction(door: Dictionary, _all_rooms: Array) -> String:
if abs(dy) > abs(dx): if abs(dy) > abs(dx):
# Vertical alignment # Vertical alignment
if dy < 0: if dy < 0:
return "Up" # room2 is above room1 return "Up" # room2 is above room1 - door on top wall of room1
else: else:
return "Down" # room2 is below room1 return "Down" # room2 is below room1 - door on bottom wall of room1
else: else:
# Horizontal alignment # Horizontal alignment
if dx < 0: if dx < 0:
return "Left" # room2 is left of room1 return "Left" # room2 is left of room1 - door on left wall of room1
else: else:
return "Right" # room2 is right of room1 return "Right" # room2 is right of room1 - door on right wall of room1
func _determine_door_direction_for_puzzle_room(door: Dictionary, puzzle_room: Dictionary, _all_rooms: Array) -> String:
# Determine which WALL of the PUZZLE ROOM the door is on
# CRITICAL: door.x and door.y are the position in room1, not necessarily in the puzzle room
# Need to check which room is the puzzle room and determine the wall based on door direction
var door_room1 = door.room1 if ("room1" in door and door.room1 and not door.room1.is_empty()) else null
var door_room2 = door.room2 if ("room2" in door and door.room2 and not door.room2.is_empty()) else null
# Check which room is the puzzle room
var puzzle_is_room1 = false
var puzzle_is_room2 = false
if door_room1:
puzzle_is_room1 = (door_room1.x == puzzle_room.x and door_room1.y == puzzle_room.y and \
door_room1.w == puzzle_room.w and door_room1.h == puzzle_room.h)
if door_room2:
puzzle_is_room2 = (door_room2.x == puzzle_room.x and door_room2.y == puzzle_room.y and \
door_room2.w == puzzle_room.w and door_room2.h == puzzle_room.h)
# Get door direction from door.dir
if "dir" in door:
var door_dir = door.dir
if puzzle_is_room1:
# Puzzle room is room1 - door.dir represents the wall of puzzle room directly
match door_dir:
"E": return "Right" # Door on right wall of puzzle room
"W": return "Left" # Door on left wall of puzzle room
"N": return "Up" # Door on top wall of puzzle room
"S": return "Down" # Door on bottom wall of puzzle room
elif puzzle_is_room2:
# Puzzle room is room2 - door.dir is FROM room1, so flip it
match door_dir:
"E": return "Left" # Door on left wall of puzzle room (room2 is to the right of room1)
"W": return "Right" # Door on right wall of puzzle room (room2 is to the left of room1)
"N": return "Down" # Door on bottom wall of puzzle room (room2 is below room1)
"S": return "Up" # Door on top wall of puzzle room (room2 is above room1)
# Fallback: calculate based on door position relative to puzzle room center
var door_x = door.x
var door_y = door.y
var puzzle_center_x = puzzle_room.x + puzzle_room.w / 2.0
var puzzle_center_y = puzzle_room.y + puzzle_room.h / 2.0
var dx = door_x - puzzle_center_x
var dy = door_y - puzzle_center_y
# Determine which wall based on which direction from center
if abs(dy) > abs(dx):
# Vertical - door is more above/below than left/right
if dy < 0:
return "Up" # Door is above puzzle room center - door is on top wall
else:
return "Down" # Door is below puzzle room center - door is on bottom wall
else:
# Horizontal - door is more left/right than above/below
if dx < 0:
return "Left" # Door is left of puzzle room center - door is on left wall
else:
return "Right" # Door is right of puzzle room center - door is on right wall

View File

@@ -11,6 +11,7 @@ var current_health: float = 50.0
var is_dead: bool = false var is_dead: bool = false
var target_player: Node = null var target_player: Node = null
var attack_timer: float = 0.0 var attack_timer: float = 0.0
var killer_player: Node = null # Track who killed this enemy (for kill credit)
# Knockback # Knockback
var is_knocked_back: bool = false var is_knocked_back: bool = false
@@ -169,7 +170,7 @@ func _check_interactable_object_collision():
var to_target = (target_player.global_position - global_position).normalized() var to_target = (target_player.global_position - global_position).normalized()
# If perpendicular dot product with target direction is negative, flip it # If perpendicular dot product with target direction is negative, flip it
if perpendicular.dot(to_target) < 0: if perpendicular.dot(to_target) < 0:
perpendicular = -perpendicular perpendicular = - perpendicular
# Apply perpendicular movement (side-step around object) # Apply perpendicular movement (side-step around object)
var side_step_velocity = perpendicular * move_speed * 0.7 # 70% of move speed for side-step var side_step_velocity = perpendicular * move_speed * 0.7 # 70% of move speed for side-step
@@ -239,6 +240,25 @@ func _find_nearest_player_in_range(max_range: float) -> Node:
return nearest return nearest
func _find_nearest_player_to_position(pos: Vector2, max_range: float = 100.0) -> Node:
# Find the nearest player to a specific position (used to find attacker)
var players = get_tree().get_nodes_in_group("player")
if players.is_empty():
return null
var nearest: Node = null
var nearest_dist = max_range
for player in players:
if not is_instance_valid(player):
continue
var dist = pos.distance_to(player.global_position)
if dist <= max_range and dist < nearest_dist:
nearest_dist = dist
nearest = player
return nearest
func take_damage(amount: float, from_position: Vector2): func take_damage(amount: float, from_position: Vector2):
# Only process damage on server/authority # Only process damage on server/authority
if not is_multiplayer_authority(): if not is_multiplayer_authority():
@@ -247,6 +267,12 @@ func take_damage(amount: float, from_position: Vector2):
if is_dead: if is_dead:
return return
# Find the nearest player to the attack position (likely the attacker)
# This allows us to credit kills correctly
var nearest_player = _find_nearest_player_to_position(from_position)
if nearest_player:
killer_player = nearest_player # Update killer to the most recent attacker
current_health -= amount current_health -= amount
print(name, " took ", amount, " damage! Health: ", current_health) print(name, " took ", amount, " damage! Health: ", current_health)
@@ -279,9 +305,21 @@ func take_damage(amount: float, from_position: Vector2):
_sync_damage_visual.rpc() _sync_damage_visual.rpc()
if current_health <= 0: if current_health <= 0:
# Prevent multiple death triggers
if is_dead:
return # Already dying
# Don't set is_dead here - let _die() set it to avoid early return bug
# Mark as dead in _die() function instead of here
# Delay death slightly so knockback is visible # Delay death slightly so knockback is visible
call_deferred("_die") call_deferred("_die")
# Notify doors that an enemy has died (if spawned from spawner)
# This needs to happen after _die() sets is_dead, so defer it
if has_meta("spawned_from_spawner") and get_meta("spawned_from_spawner"):
call_deferred("_notify_doors_enemy_died")
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
func rpc_take_damage(amount: float, from_position: Vector2): func rpc_take_damage(amount: float, from_position: Vector2):
# RPC version - only process on server/authority # RPC version - only process on server/authority
@@ -347,6 +385,27 @@ func _on_take_damage():
# Override in subclasses for custom damage reactions # Override in subclasses for custom damage reactions
pass pass
func _notify_doors_enemy_died():
# Notify all doors that require enemies to check puzzle state
# This ensures doors open immediately when the last enemy dies
if not has_meta("spawned_from_spawner") or not get_meta("spawned_from_spawner"):
return # Only notify if this enemy was spawned from a spawner
# Find all doors in the scene that require enemies
for door in get_tree().get_nodes_in_group("blocking_door"):
if not is_instance_valid(door):
continue
if not door.has_method("_check_puzzle_state"):
continue
# Check if this door requires enemies (requires_enemies is a property defined in door.gd)
# Access property directly - it's always defined in door.gd class
if door.requires_enemies:
# Trigger puzzle state check immediately (doors will verify if all enemies are dead)
door.call_deferred("_check_puzzle_state")
print("Enemy: Notified door ", door.name, " to check puzzle state after enemy death")
func _set_animation(_anim_name: String): func _set_animation(_anim_name: String):
# Virtual function - override in subclasses that use animation state system # Virtual function - override in subclasses that use animation state system
# (e.g., enemy_humanoid.gd uses player-like animation system) # (e.g., enemy_humanoid.gd uses player-like animation system)
@@ -359,6 +418,11 @@ func _die():
is_dead = true is_dead = true
print(name, " died!") print(name, " died!")
# Credit kill to the player who dealt the fatal damage
if killer_player and is_instance_valid(killer_player) and killer_player.character_stats:
killer_player.character_stats.kills += 1
print(name, " kill credited to ", killer_player.name, " (total kills: ", killer_player.character_stats.kills, ")")
# Spawn loot immediately (before death animation) # Spawn loot immediately (before death animation)
_spawn_loot() _spawn_loot()

View File

@@ -128,10 +128,10 @@ func _update_z_position(_delta):
if sprite: if sprite:
sprite.position.y = -position_z * 0.5 sprite.position.y = -position_z * 0.5
# Update shadow based on height # Update shadow based on height (use same logic as base class _update_client_visuals for consistency)
if shadow: if shadow:
var shadow_scale = 1.0 - (position_z / 50.0) * 0.5 var shadow_scale = 1.0 - (position_z / 50.0) * 0.5
shadow.scale = Vector2.ONE * max(0.3, shadow_scale) * Vector2(0.8, 0.4) shadow.scale = Vector2.ONE * max(0.3, shadow_scale)
shadow.modulate.a = 0.5 - (position_z / 50.0) * 0.2 shadow.modulate.a = 0.5 - (position_z / 50.0) * 0.2
func _update_client_visuals(): func _update_client_visuals():

View File

@@ -157,13 +157,243 @@ func _setup_appearance():
# Load random equipment/appearance # Load random equipment/appearance
_load_random_equipment() _load_random_equipment()
# Load type-specific addons # Randomize appearance (skin, hair, facial hair, eyes, eyelashes, ears)
_randomize_appearance()
# Load type-specific addons (may override ears from randomization)
_load_type_addons() _load_type_addons()
# Load random beastkin addons (low chance) # Load random beastkin addons (low chance, can override type addon)
if appearance_rng.randf() < 0.1: # 10% chance if appearance_rng.randf() < 0.1: # 10% chance
_load_beastkin_addon() _load_beastkin_addon()
func _randomize_appearance():
# Randomize skin (for humanoids only - other types already have specific skins)
if humanoid_type == HumanoidType.HUMANOID:
var skin_value = appearance_rng.randi_range(0, 6) # 0-6 for Human1-Human7
_set_skin(skin_value)
# Randomize hair (0-12, 0 = no hair)
var hair_type = appearance_rng.randi_range(0, 12)
_set_hair(hair_type)
# Randomize facial hair (0-3, 0 = no facial hair)
var facial_hair_type = appearance_rng.randi_range(0, 3)
_set_facial_hair(facial_hair_type)
# Randomize eyes (1-14, 1-14 are eye colors)
var eye_type = appearance_rng.randi_range(1, 14)
_set_eyes(eye_type)
# Randomize eyelashes (0-8, 0 = no eyelashes)
var eyelash_type = appearance_rng.randi_range(0, 8)
_set_eye_lashes(eyelash_type)
# Randomize ears (0-7, 0 = no ears, 1-7 are elf ears)
# Note: Type-specific addons may override this
if humanoid_type == HumanoidType.HUMANOID:
# Only randomize ears for regular humanoids (other types have type-specific ears)
var ear_type = appearance_rng.randi_range(0, 7)
_set_ears(ear_type)
# Randomize hair color (bright colors for enemies)
var hair_colors = [
Color.WHITE, Color(0.9, 0.9, 0.9), Color(0.7, 0.7, 0.7), # White/Gray
Color(0.5, 0.3, 0.2), Color(0.3, 0.2, 0.1), # Brown/Black
Color(0.9, 0.7, 0.4), Color(0.8, 0.6, 0.3) # Blonde
]
var hair_color = hair_colors[appearance_rng.randi() % hair_colors.size()]
_set_hair_color(hair_color)
# Set facial hair color to match hair color or slightly different
var facial_hair_color = hair_color
if appearance_rng.randf() < 0.3: # 30% chance for slightly different color
facial_hair_color = hair_colors[appearance_rng.randi() % hair_colors.size()]
_set_facial_hair_color(facial_hair_color)
func _set_skin(i_value: int):
if i_value < 0 or i_value > 6:
return
if sprite_body:
var skin_path = "res://assets/gfx/Puny-Characters/Layer 0 - Skins/Human" + str(i_value + 1) + ".png"
var skin_texture = load(skin_path)
if skin_texture:
sprite_body.texture = skin_texture
sprite_body.hframes = 35
sprite_body.vframes = 8
func _set_facial_hair(i_type: int):
if i_type < 0 or i_type > 3:
return
if not sprite_facial_hair:
return
if i_type == 0:
sprite_facial_hair.texture = null
return
var facial_hair_path = ""
match i_type:
1:
facial_hair_path = "res://assets/gfx/Puny-Characters/Layer 4 - Hairstyle/Facial Hairstyles/Beardstyle1White.png"
2:
facial_hair_path = "res://assets/gfx/Puny-Characters/Layer 4 - Hairstyle/Facial Hairstyles/Beardstyle2White.png"
3:
facial_hair_path = "res://assets/gfx/Puny-Characters/Layer 4 - Hairstyle/Facial Hairstyles/Mustache1White.png"
if facial_hair_path != "":
var facial_hair_texture = load(facial_hair_path)
if facial_hair_texture:
sprite_facial_hair.texture = facial_hair_texture
sprite_facial_hair.hframes = 35
sprite_facial_hair.vframes = 8
func _set_hair(i_type: int):
if i_type < 0 or i_type > 12:
return
if not sprite_hair:
return
if i_type == 0:
sprite_hair.texture = null
return
var hairstyle_path = ""
if i_type >= 1 and i_type <= 4:
# Female hairstyles
hairstyle_path = "res://assets/gfx/Puny-Characters/Layer 4 - Hairstyle/Hairstyles/FHairstyle" + str(i_type) + "White.png"
elif i_type >= 5:
# Male hairstyles (5-12 maps to MHairstyle1-8)
var male_hairstyle = i_type - 4
hairstyle_path = "res://assets/gfx/Puny-Characters/Layer 4 - Hairstyle/Hairstyles/MHairstyle" + str(male_hairstyle) + "White.png"
if hairstyle_path != "":
var hair_texture = load(hairstyle_path)
if hair_texture:
sprite_hair.texture = hair_texture
sprite_hair.hframes = 35
sprite_hair.vframes = 8
func _set_eye_lashes(i_eyelashes: int):
if i_eyelashes < 0 or i_eyelashes > 8:
return
if not sprite_eyelashes:
return
if i_eyelashes == 0:
sprite_eyelashes.texture = null
return
var eyelash_path = ""
match i_eyelashes:
1:
eyelash_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eyelashes/FEyelash1.png"
2:
eyelash_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eyelashes/FEyelash2.png"
3:
eyelash_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eyelashes/FEyelash3.png"
4:
eyelash_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eyelashes/MEyelash1.png"
5:
eyelash_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eyelashes/MEyelash2.png"
6:
eyelash_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eyelashes/NEyelash1.png"
7:
eyelash_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eyelashes/NEyelash2.png"
8:
eyelash_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eyelashes/NEyelash3.png"
if eyelash_path != "":
var eyelash_texture = load(eyelash_path)
if eyelash_texture:
sprite_eyelashes.texture = eyelash_texture
sprite_eyelashes.hframes = 35
sprite_eyelashes.vframes = 8
func _set_eyes(i_eyes: int):
if i_eyes < 0 or i_eyes > 14:
return
if not sprite_eyes:
return
if i_eyes == 0:
sprite_eyes.texture = null
return
var eye_path = ""
match i_eyes:
1:
eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorBlack.png"
2:
eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorBlue.png"
3:
eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorCyan.png"
4:
eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorDarkBlue.png"
5:
eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorDarkCyan.png"
6:
eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorDarkLime.png"
7:
eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorDarkRed.png"
8:
eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorFullBlack.png"
9:
eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorFullWhite.png"
10:
eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorGray.png"
11:
eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorLightLime.png"
12:
eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorOrange.png"
13:
eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorRed.png"
14:
eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorYellow.png"
if eye_path != "":
var eye_texture = load(eye_path)
if eye_texture:
sprite_eyes.texture = eye_texture
sprite_eyes.hframes = 35
sprite_eyes.vframes = 8
func _set_ears(i_ears: int):
if i_ears < 0 or i_ears > 7:
return
if not sprite_addons:
return
if i_ears == 0:
# Don't clear addons if we're not a humanoid (other types need their addons)
# For humanoids, clear only if no type-specific addon is needed
if humanoid_type == HumanoidType.HUMANOID:
# Will be set by _load_type_addons if needed, otherwise stays null
pass
return
# Only set elf ears if we're a humanoid (other types have type-specific addons)
if humanoid_type == HumanoidType.HUMANOID:
var ear_path = "res://assets/gfx/Puny-Characters/Layer 7 - Add-ons/Elf Add-ons/ElfEars" + str(i_ears) + ".png"
var ear_texture = load(ear_path)
if ear_texture:
sprite_addons.texture = ear_texture
sprite_addons.hframes = 35
sprite_addons.vframes = 8
func _set_facial_hair_color(i_color: Color):
if sprite_facial_hair:
sprite_facial_hair.modulate = i_color
func _set_hair_color(i_color: Color):
if sprite_hair:
sprite_hair.modulate = i_color
func _get_body_texture_for_type(type: HumanoidType) -> String: func _get_body_texture_for_type(type: HumanoidType) -> String:
match type: match type:
HumanoidType.CYCLOPS: HumanoidType.CYCLOPS:
@@ -188,9 +418,175 @@ func _get_body_texture_for_type(type: HumanoidType) -> String:
return "" return ""
func _load_random_equipment(): func _load_random_equipment():
# Load random shoes, armour, etc. (can be empty) # Load random equipment (shoes, clothes, gloves, headgear)
# For now, just load basic stuff - can be expanded later # Equipment is optional - chance to have each piece
pass
# Random shoes (Layer 1 - Shoes)
if appearance_rng.randf() < 0.8: # 80% chance to have shoes
_load_random_shoes()
# Random clothes/armour (Layer 2 - Clothes)
if appearance_rng.randf() < 0.7: # 70% chance to have clothes
_load_random_clothes()
# Random gloves (Layer 3 - Gloves)
if appearance_rng.randf() < 0.6: # 60% chance to have gloves
_load_random_gloves()
# Random headgear (Layer 6 - Headgears)
if appearance_rng.randf() < 0.5: # 50% chance to have headgear
_load_random_headgear()
func _load_random_shoes():
if not sprite_boots:
return
# Available shoes
var shoes = [
"ShoesBrown.png",
"ShoesMaple.png",
"IronBoots.png"
]
var shoe_file = shoes[appearance_rng.randi() % shoes.size()]
var shoe_path = "res://assets/gfx/Puny-Characters/Layer 1 - Shoes/" + shoe_file
var shoe_texture = load(shoe_path)
if shoe_texture:
sprite_boots.texture = shoe_texture
sprite_boots.hframes = 35
sprite_boots.vframes = 8
func _load_random_clothes():
if not sprite_armour:
return
# Available clothes from different categories (using actual files found)
var clothes_categories = {
"Basic Body": [
"BasicRed.png", "BasicBlue.png", "BasicGreen.png",
"BasicYellow.png", "BasicBlack.png", "BasicCyan.png",
"BasicPurple.png"
],
"Tunic Body": [
"BrownTunic.png", "LeatherTunic.png", "LimeTunic.png",
"LinenTunic.png", "WoolTunic.png"
],
"Armour Body": [
"BronzeArmour.png", "GoldArmour.png", "IronArmour.png",
"SteelArmour.png"
],
"Viking Body": [
"JarlBody.png", "KarlBody.png", "WarriorBody.png"
],
"Mongol Body": [
"Cherbi.png", "Khaan.png", "Kheshig.png", "Noyon.png"
],
"French Body": [
"HunterIndigo.png", "MusketeerPurple.png", "MusketeerRed.png"
],
"Japanese Body": [
"NinjaBlack.png", "RoninBlue.png", "RoninRed.png",
"SamuraiLime.png", "SamuraiPurple.png", "TravellerCyan.png",
"YabusameBlue.png", "YabusameRed.png"
]
}
# Choose a random category
var category_keys = clothes_categories.keys()
var selected_category = category_keys[appearance_rng.randi() % category_keys.size()]
var clothes_in_category = clothes_categories[selected_category]
var selected_cloth = clothes_in_category[appearance_rng.randi() % clothes_in_category.size()]
var cloth_path = "res://assets/gfx/Puny-Characters/Layer 2 - Clothes/" + selected_category + "/" + selected_cloth
var cloth_texture = load(cloth_path)
if cloth_texture:
sprite_armour.texture = cloth_texture
sprite_armour.hframes = 35
sprite_armour.vframes = 8
func _load_random_gloves():
# Note: Gloves are in Layer 3, but we only have sprite_armour for clothes
# Since gloves are typically overlaid on clothes, we could:
# 1. Load gloves as a separate layer if we add one
# 2. Skip gloves if we already have clothes (gloves are optional)
# 3. Combine gloves with clothes (would require composition)
# For now, we'll skip gloves if clothes are already loaded
# TODO: Consider adding a separate sprite layer for gloves if needed
if not sprite_armour:
return
# Only load gloves if we don't already have clothes
# This prevents gloves from overriding clothes
if sprite_armour.texture:
return # Already have clothes, skip gloves
# Available gloves
var gloves = [
"GlovesBrown.png",
"GlovesLightBrown.png",
"GlovesMaple.png",
"IronGloves.png"
]
var glove_file = gloves[appearance_rng.randi() % gloves.size()]
var glove_path = "res://assets/gfx/Puny-Characters/Layer 3 - Gloves/" + glove_file
var glove_texture = load(glove_path)
if glove_texture:
sprite_armour.texture = glove_texture
sprite_armour.hframes = 35
sprite_armour.vframes = 8
func _load_random_headgear():
if not sprite_headgear:
return
# Available headgears organized by category (using actual files found)
var headgear_categories = {
"": ["Headband.png"], # Direct files in Layer 6 - Headgears
"Basic Assasin": [
"AssasinBandanaBlack.png", "StalkerHoodBlack.png", "ThiefBandanaGreen.png"
],
"Basic Mage": [
"EsperHatBlue.png", "HighMageHatCyan.png", "MageHatRed.png", "SorcererHoodCyan.png"
],
"Basic Melee": [
"DarkKnightHelm.png", "DragonKnightHelm.png", "GruntHelm.png", "KnightHelm.png",
"NoviceHelm.png", "PaladinHelmCyan.png", "ScoutHelmGreen.png",
"SoldierBronzeHelmBlue.png", "SoldierBronzeHelmRed.png", "SoldierGoldHelmBlue.png",
"SoldierIronHelmBlue.png", "SoldierSteelHelmBlue.png"
],
"Basic Range": [
"ArcherHatCyan.png", "HunterHatRed.png", "RangerHatGreen.png", "RogueHatGreen.png"
],
"French": ["MusketeerHatPurple.png"],
"Japanese": [
"DaimyoHelm.png", "NinjaBandanaBlue.png", "RoninStrawhatBlue.png",
"SamuraiHelm.png", "ShogunHelmPurple.png", "TravellerStrawhat.png",
"TravellerStrawhatCyan.png", "VillagerStrawhat.png", "YabusameStrawhatBlue.png"
],
"Mongol": [
"CherbiHelm.png", "KhaanHelm.png", "KheshigHelm.png", "NoyonHelm.png"
],
"Viking": ["JarlHelm.png", "KarlHelm.png"]
}
# Choose a random category
var category_keys = headgear_categories.keys()
var selected_category = category_keys[appearance_rng.randi() % category_keys.size()]
var headgears_in_category = headgear_categories[selected_category]
var selected_headgear = headgears_in_category[appearance_rng.randi() % headgears_in_category.size()]
# Build path - add subdirectory if category is not empty
var headgear_path = "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/"
if selected_category != "":
headgear_path += selected_category + "/"
headgear_path += selected_headgear
var headgear_texture = load(headgear_path)
if headgear_texture:
sprite_headgear.texture = headgear_texture
sprite_headgear.hframes = 35
sprite_headgear.vframes = 8
func _load_type_addons(): func _load_type_addons():
# Load type-specific addons based on requirements # Load type-specific addons based on requirements
@@ -224,15 +620,9 @@ func _load_type_addons():
print(name, " loaded skeleton horns: ", addon_path) print(name, " loaded skeleton horns: ", addon_path)
HumanoidType.HUMANOID: HumanoidType.HUMANOID:
# Can have (but not must) ElfEars # Ears are already set by _randomize_appearance, so skip here
if appearance_rng.randf() < 0.3 and sprite_addons: # Type-specific addons would override if needed
var addon_path = "res://assets/gfx/Puny-Characters/Layer 7 - Add-ons/Elf Add-ons/ElfEars1.png" pass
var texture = load(addon_path)
if texture:
sprite_addons.texture = texture
sprite_addons.hframes = 35
sprite_addons.vframes = 8
print(name, " loaded elf ears")
HumanoidType.NIGHTELF: HumanoidType.NIGHTELF:
# Must have NightElfEars7 # Must have NightElfEars7

View File

@@ -10,13 +10,14 @@ extends Node2D
var spawned_enemies: Array = [] var spawned_enemies: Array = []
var respawn_timer: float = 0.0 var respawn_timer: float = 0.0
var smoke_puff_scene = preload("res://scenes/smoke_puff.tscn") var smoke_puff_scene = preload("res://scenes/smoke_puff.tscn")
var has_ever_spawned: bool = false # Track if this spawner has ever spawned an enemy
func _ready(): func _ready():
print("========== EnemySpawner READY ==========") print("========== EnemySpawner READY ==========")
print(" Position: ", global_position) print(" Position: ", global_position)
print(" Is server: ", multiplayer.is_server()) print(" Is server: ", multiplayer.is_server())
print(" Has multiplayer peer: ", multiplayer.has_multiplayer_peer()) print(" Has multiplayer peer: ", multiplayer.has_multiplayer_peer())
print(" Is authority: ", is_multiplayer_authority() if multiplayer.has_multiplayer_peer() else "N/A") print(" Is authority: ", str(is_multiplayer_authority()) if multiplayer.has_multiplayer_peer() else "N/A")
print(" spawn_on_ready: ", spawn_on_ready) print(" spawn_on_ready: ", spawn_on_ready)
print(" max_enemies: ", max_enemies) print(" max_enemies: ", max_enemies)
print(" enemy_scenes.size(): ", enemy_scenes.size()) print(" enemy_scenes.size(): ", enemy_scenes.size())
@@ -45,8 +46,9 @@ func _process(delta):
# Clean up dead enemies from list # Clean up dead enemies from list
spawned_enemies = spawned_enemies.filter(func(e): return is_instance_valid(e) and not e.is_dead) spawned_enemies = spawned_enemies.filter(func(e): return is_instance_valid(e) and not e.is_dead)
# Check if we need to respawn # Check if we need to respawn (only if respawn_time > 0)
if spawned_enemies.size() < max_enemies: # Puzzle spawners have respawn_time = 0.0, so they won't respawn
if respawn_time > 0.0 and spawned_enemies.size() < max_enemies:
respawn_timer += delta respawn_timer += delta
if respawn_timer >= respawn_time: if respawn_timer >= respawn_time:
spawn_enemy() spawn_enemy()
@@ -95,19 +97,40 @@ func spawn_enemy():
# Spawn multiple smoke puffs at slightly different positions # Spawn multiple smoke puffs at slightly different positions
var smoke_puffs = [] var smoke_puffs = []
var puff_spawn_radius = 8.0 # Pixels - spawn puffs in a small area around spawner var puff_spawn_radius = 8.0 # Pixels - spawn puffs in a small area around spawner
var puff_positions = [] # Store positions for syncing to clients
# Calculate puff positions first
for i in range(num_puffs): for i in range(num_puffs):
var puff_offset = Vector2( var puff_offset = Vector2(
randf_range(-puff_spawn_radius, puff_spawn_radius), randf_range(-puff_spawn_radius, puff_spawn_radius),
randf_range(-puff_spawn_radius, puff_spawn_radius) randf_range(-puff_spawn_radius, puff_spawn_radius)
) )
var puff = _spawn_smoke_puff_at_position(global_position + puff_offset) puff_positions.append(global_position + puff_offset)
# Spawn smoke puffs on server
for puff_pos in puff_positions:
var puff = _spawn_smoke_puff_at_position(puff_pos)
if puff: if puff:
smoke_puffs.append(puff) smoke_puffs.append(puff)
# Sync smoke puffs to all clients
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
var game_world = get_tree().get_first_node_in_group("game_world")
if not game_world:
# Fallback: traverse up the tree to find GameWorld
var node = get_parent()
while node:
if node.has_method("_sync_smoke_puffs"):
game_world = node
break
node = node.get_parent()
if game_world and game_world.has_method("_sync_smoke_puffs"):
game_world._sync_smoke_puffs.rpc(name, puff_positions)
# Wait for smoke puffs to finish animating before spawning enemy # Wait for smoke puffs to finish animating before spawning enemy
# Smoke puff animation: 4 frames * (1/10.0) seconds per frame = 0.4s, plus move_duration 1.5s, plus fade_duration 0.5s = ~2.4s total # Reduced duration for faster spawning: 4 frames * (1/10.0) seconds per frame = 0.4s, plus move_duration 1.0s, plus fade_duration 0.3s = ~1.7s total
var smoke_animation_duration = (4.0 / 10.0) + 1.5 + 0.5 # Total animation time var smoke_animation_duration = (4.0 / 10.0) + 1.0 + 0.3 # Reduced from 2.4s to 1.7s
await get_tree().create_timer(smoke_animation_duration).timeout await get_tree().create_timer(smoke_animation_duration).timeout
print(" Smoke puffs finished - now spawning enemy...") print(" Smoke puffs finished - now spawning enemy...")
@@ -126,6 +149,27 @@ func spawn_enemy():
enemy.spawn_position = global_position enemy.spawn_position = global_position
print(" Set enemy position to: ", global_position) print(" Set enemy position to: ", global_position)
# If it's a humanoid enemy, randomize the humanoid_type
var humanoid_type = null
if scene_to_spawn.resource_path.ends_with("enemy_humanoid.tscn"):
# Random humanoid type: 0=CYCLOPS, 1=DEMON, 2=HUMANOID, 3=NIGHTELF, 4=GOBLIN, 5=ORC, 6=SKELETON
# Weight towards common types (goblins, humans, orcs) - 40% goblin, 30% humanoid, 20% orc, 10% other
var rand_val = randf()
var type_value = 2 # Default to HUMANOID
if rand_val < 0.4:
type_value = 4 # GOBLIN (40%)
elif rand_val < 0.7:
type_value = 2 # HUMANOID (30%)
elif rand_val < 0.9:
type_value = 5 # ORC (20%)
else:
# 10% for other types (distributed evenly)
var other_types = [0, 1, 3, 6] # CYCLOPS, DEMON, NIGHTELF, SKELETON
type_value = other_types[randi() % other_types.size()]
enemy.humanoid_type = type_value
humanoid_type = type_value
print(" Randomized humanoid_type: ", type_value)
# CRITICAL: Mark this enemy as spawned from a spawner (for door puzzle tracking) # CRITICAL: Mark this enemy as spawned from a spawner (for door puzzle tracking)
enemy.set_meta("spawned_from_spawner", true) enemy.set_meta("spawned_from_spawner", true)
enemy.set_meta("spawner_name", name) enemy.set_meta("spawner_name", name)
@@ -167,6 +211,7 @@ func spawn_enemy():
enemy.set_meta("spawn_scene_index", scene_index) enemy.set_meta("spawn_scene_index", scene_index)
spawned_enemies.append(enemy) spawned_enemies.append(enemy)
has_ever_spawned = true # Mark that this spawner has spawned at least once
print(" ✓ Successfully spawned enemy: ", enemy.name, " at ", global_position, " scene_index: ", scene_index) print(" ✓ Successfully spawned enemy: ", enemy.name, " at ", global_position, " scene_index: ", scene_index)
print(" Total spawned enemies: ", spawned_enemies.size()) print(" Total spawned enemies: ", spawned_enemies.size())
@@ -191,16 +236,19 @@ func spawn_enemy():
if game_world and game_world.has_method("_sync_enemy_spawn"): if game_world and game_world.has_method("_sync_enemy_spawn"):
# Use spawner name for identification # Use spawner name for identification
print(" DEBUG: Calling _sync_enemy_spawn.rpc with name=", name, " pos=", global_position, " scene_index=", scene_index) # Pass humanoid_type if it's a humanoid enemy (for syncing to clients)
game_world._sync_enemy_spawn.rpc(name, global_position, scene_index) var sync_humanoid_type = humanoid_type if humanoid_type != null else -1
print(" Sent RPC to sync enemy spawn to clients: spawner=", name, " pos=", global_position, " scene_index=", scene_index) print(" DEBUG: Calling _sync_enemy_spawn.rpc with name=", name, " pos=", global_position, " scene_index=", scene_index, " humanoid_type=", sync_humanoid_type)
game_world._sync_enemy_spawn.rpc(name, global_position, scene_index, sync_humanoid_type)
print(" Sent RPC to sync enemy spawn to clients: spawner=", name, " pos=", global_position, " scene_index=", scene_index, " humanoid_type=", sync_humanoid_type)
else: else:
var has_method_str = str(game_world.has_method("_sync_enemy_spawn")) if game_world else "N/A" var has_method_str = str(game_world.has_method("_sync_enemy_spawn")) if game_world else "N/A"
push_error("ERROR: Could not find GameWorld or _sync_enemy_spawn method! game_world=", game_world, " has_method=", has_method_str) push_error("ERROR: Could not find GameWorld or _sync_enemy_spawn method! game_world=", game_world, " has_method=", has_method_str)
func spawn_enemy_at_position(spawn_pos: Vector2, scene_index: int = -1): func spawn_enemy_at_position(spawn_pos: Vector2, scene_index: int = -1, humanoid_type: int = -1):
# This method is called by GameWorld RPC to spawn enemies on clients # This method is called by GameWorld RPC to spawn enemies on clients
# scene_index tells us which scene from enemy_scenes array was used on the server # scene_index tells us which scene from enemy_scenes array was used on the server
# humanoid_type tells us the humanoid type if it's a humanoid enemy (for syncing from server)
var scene_to_spawn: PackedScene = null var scene_to_spawn: PackedScene = null
if scene_index >= 0 and scene_index < enemy_scenes.size(): if scene_index >= 0 and scene_index < enemy_scenes.size():
# Use the scene index that was synced from server # Use the scene index that was synced from server
@@ -215,10 +263,10 @@ func spawn_enemy_at_position(spawn_pos: Vector2, scene_index: int = -1):
push_error("ERROR: Spawner has no enemy scenes set! Add scenes to enemy_scenes array.") push_error("ERROR: Spawner has no enemy scenes set! Add scenes to enemy_scenes array.")
return return
print("Client: spawn_enemy_at_position called at ", spawn_pos) print("Client: spawn_enemy_at_position called at ", spawn_pos, " humanoid_type: ", humanoid_type)
# Spawn smoke puff effect # NOTE: Smoke puffs are synced via RPC (_sync_smoke_puffs) from server
_spawn_smoke_puff() # so we don't spawn them here - they're already spawned by the RPC handler
# Instantiate and add enemy # Instantiate and add enemy
var enemy = scene_to_spawn.instantiate() var enemy = scene_to_spawn.instantiate()
@@ -231,6 +279,11 @@ func spawn_enemy_at_position(spawn_pos: Vector2, scene_index: int = -1):
if "spawn_position" in enemy: if "spawn_position" in enemy:
enemy.spawn_position = spawn_pos enemy.spawn_position = spawn_pos
# If it's a humanoid enemy, set the humanoid_type from server
if humanoid_type >= 0 and scene_to_spawn.resource_path.ends_with("enemy_humanoid.tscn"):
enemy.humanoid_type = humanoid_type
print("Client: Set humanoid_type to ", humanoid_type)
# CRITICAL: Set collision mask BEFORE adding to scene to ensure enemies collide with walls (layer 7 = bit 6 = 64) # CRITICAL: Set collision mask BEFORE adding to scene to ensure enemies collide with walls (layer 7 = bit 6 = 64)
# This overrides any collision_mask set in the scene file # This overrides any collision_mask set in the scene file
enemy.collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64) enemy.collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64)
@@ -255,14 +308,20 @@ func spawn_enemy_at_position(spawn_pos: Vector2, scene_index: int = -1):
print(" ✓ Client spawned enemy: ", enemy.name, " at ", spawn_pos) print(" ✓ Client spawned enemy: ", enemy.name, " at ", spawn_pos)
func get_spawned_enemy_positions() -> Array: func get_spawned_enemy_positions() -> Array:
# Return array of dictionaries with position and scene_index for all currently spawned enemies # Return array of dictionaries with position, scene_index, and humanoid_type for all currently spawned enemies
var enemy_data = [] var enemy_data = []
for enemy in spawned_enemies: for enemy in spawned_enemies:
if is_instance_valid(enemy) and not enemy.is_dead: if is_instance_valid(enemy) and not enemy.is_dead:
var scene_index = -1 var scene_index = -1
if enemy.has_meta("spawn_scene_index"): if enemy.has_meta("spawn_scene_index"):
scene_index = enemy.get_meta("spawn_scene_index") scene_index = enemy.get_meta("spawn_scene_index")
enemy_data.append({"position": enemy.global_position, "scene_index": scene_index}) var data = {"position": enemy.global_position, "scene_index": scene_index}
# Include humanoid_type if it's a humanoid enemy
if "humanoid_type" in enemy:
data["humanoid_type"] = enemy.humanoid_type
else:
data["humanoid_type"] = -1
enemy_data.append(data)
return enemy_data return enemy_data
func _verify_enemy_collision_mask(enemy: Node): func _verify_enemy_collision_mask(enemy: Node):
@@ -297,10 +356,17 @@ func _spawn_smoke_puff_at_position(puff_position: Vector2) -> Node:
var puff = smoke_puff_scene.instantiate() var puff = smoke_puff_scene.instantiate()
if puff: if puff:
puff.global_position = puff_position puff.global_position = puff_position
var parent = get_parent() # Ensure smoke puff is visible - set high z_index so it appears above ground
puff.z_index = 10 # High z-index to ensure visibility
if puff.has_node("Sprite2D"):
puff.get_node("Sprite2D").z_index = 10
# Add to Entities node (same as enemies) for proper layering
var entities_node = get_parent().get_node_or_null("Entities")
var parent = entities_node if entities_node else get_parent()
if parent: if parent:
parent.add_child(puff) parent.add_child(puff)
print(" ✓ Smoke puff spawned at ", puff_position) print(" ✓ Smoke puff spawned at ", puff_position, " z_index: ", puff.z_index)
return puff return puff
else: else:
print(" ERROR: No parent node for smoke puff!") print(" ERROR: No parent node for smoke puff!")

View File

@@ -68,6 +68,7 @@ func _on_body_entered(body):
if object_type == "Pillar" and not is_being_held: if object_type == "Pillar" and not is_being_held:
var weight = _get_object_weight(body) var weight = _get_object_weight(body)
if weight >= required_weight: # Pillar must have weight >= 5.0 if weight >= required_weight: # Pillar must have weight >= 5.0
$PressSwitch.play()
objects_on_switch.append(body) objects_on_switch.append(body)
current_weight += weight current_weight += weight
print("FloorSwitch: Pillar entered switch (not held), weight: ", weight, ", total: ", current_weight) print("FloorSwitch: Pillar entered switch (not held), weight: ", weight, ", total: ", current_weight)
@@ -81,6 +82,7 @@ func _on_body_entered(body):
if body.is_in_group("player") or (body.has_method("can_be_grabbed") and body.can_be_grabbed()): if body.is_in_group("player") or (body.has_method("can_be_grabbed") and body.can_be_grabbed()):
var weight = _get_object_weight(body) var weight = _get_object_weight(body)
if weight > 0: if weight > 0:
$PressSwitch.play()
objects_on_switch.append(body) objects_on_switch.append(body)
current_weight += weight current_weight += weight
_check_activation() _check_activation()
@@ -91,12 +93,13 @@ func _on_body_exited(body):
# For pillar switches, verify the object is still valid (not being held now) # For pillar switches, verify the object is still valid (not being held now)
if switch_type == "pillar": if switch_type == "pillar":
var object_type = body.get("object_type") if "object_type" in body else "" var object_type = body.get("object_type") if "object_type" in body else ""
var is_being_held = body.get("is_being_held") if "is_being_held" in body else false var _is_being_held = body.get("is_being_held") if "is_being_held" in body else false
# Only remove if it was a pillar (and might now be held) # Only remove if it was a pillar (and might now be held)
if object_type == "Pillar": if object_type == "Pillar":
var weight = _get_object_weight(body) var weight = _get_object_weight(body)
if weight > 0: if weight > 0:
$ReleaseSwitch.play()
objects_on_switch.erase(body) objects_on_switch.erase(body)
current_weight -= weight current_weight -= weight
print("FloorSwitch: Pillar left switch, weight: ", weight, ", total: ", current_weight) print("FloorSwitch: Pillar left switch, weight: ", weight, ", total: ", current_weight)
@@ -105,6 +108,7 @@ func _on_body_exited(body):
# Walk switch: Remove any object # Walk switch: Remove any object
var weight = _get_object_weight(body) var weight = _get_object_weight(body)
if weight > 0: if weight > 0:
$ReleaseSwitch.play()
objects_on_switch.erase(body) objects_on_switch.erase(body)
current_weight -= weight current_weight -= weight
_check_activation() _check_activation()

View File

@@ -86,7 +86,12 @@ func _on_player_connected(peer_id: int, player_info: Dictionary):
_sync_spawn_player.rpc(peer_id, player_info.local_player_count) _sync_spawn_player.rpc(peer_id, player_info.local_player_count)
# Sync existing enemies (from spawners) to the new client # Sync existing enemies (from spawners) to the new client
_sync_existing_enemies_to_client(peer_id) # Wait a bit after dungeon sync to ensure spawners are spawned first
call_deferred("_sync_existing_enemies_to_client", peer_id)
# Sync existing chest open states to the new client
# Wait a bit after dungeon sync to ensure objects are spawned first
call_deferred("_sync_existing_chest_states_to_client", peer_id)
# Note: Dungeon-spawned enemies are already synced via _sync_dungeon RPC # Note: Dungeon-spawned enemies are already synced via _sync_dungeon RPC
# which includes dungeon_data.enemies and calls _spawn_enemies() on the client. # which includes dungeon_data.enemies and calls _spawn_enemies() on the client.
@@ -94,12 +99,10 @@ func _on_player_connected(peer_id: int, player_info: Dictionary):
# Note: Interactable objects are also synced via _sync_dungeon RPC # Note: Interactable objects are also synced via _sync_dungeon RPC
# which includes dungeon_data.interactable_objects and calls _spawn_interactable_objects() on the client. # which includes dungeon_data.interactable_objects and calls _spawn_interactable_objects() on the client.
# However, chest open states need to be synced separately since they change during gameplay.
# Note: Interactable objects are also synced via _sync_dungeon RPC
# which includes dungeon_data.interactable_objects and calls _spawn_interactable_objects() on the client.
# Sync existing torches to the new client # Sync existing torches to the new client
_sync_existing_torches_to_client(peer_id) call_deferred("_sync_existing_torches_to_client", peer_id)
else: else:
# Clients spawn directly when they receive this signal # Clients spawn directly when they receive this signal
print("GameWorld: Client spawning players for peer ", peer_id) print("GameWorld: Client spawning players for peer ", peer_id)
@@ -107,9 +110,18 @@ func _on_player_connected(peer_id: int, player_info: Dictionary):
func _sync_existing_enemies_to_client(client_peer_id: int): func _sync_existing_enemies_to_client(client_peer_id: int):
# Find all enemy spawners and sync their spawned enemies to the new client # Find all enemy spawners and sync their spawned enemies to the new client
# Spawners are children of the Entities node, not GameWorld directly
var spawners = [] var spawners = []
var entities_node = get_node_or_null("Entities")
if entities_node:
# Find spawners in Entities node
for child in entities_node.get_children():
if child.is_in_group("enemy_spawner") or (child.has_method("get_spawned_enemy_positions") and child.has_method("spawn_enemy_at_position")):
spawners.append(child)
else:
# Fallback: search all children (shouldn't happen, but just in case)
for child in get_children(): for child in get_children():
if child.has_method("get_spawned_enemy_positions") and child.has_method("spawn_enemy_at_position"): if child.is_in_group("enemy_spawner") or (child.has_method("get_spawned_enemy_positions") and child.has_method("spawn_enemy_at_position")):
spawners.append(child) spawners.append(child)
print("GameWorld: Syncing existing enemies to client ", client_peer_id, " from ", spawners.size(), " spawners") print("GameWorld: Syncing existing enemies to client ", client_peer_id, " from ", spawners.size(), " spawners")
@@ -117,11 +129,12 @@ func _sync_existing_enemies_to_client(client_peer_id: int):
for spawner in spawners: for spawner in spawners:
var enemy_data = spawner.get_spawned_enemy_positions() var enemy_data = spawner.get_spawned_enemy_positions()
for data in enemy_data: for data in enemy_data:
# Use the stored scene_index for each enemy # Use the stored scene_index and humanoid_type for each enemy
var pos = data.position var pos = data.position
var scene_index = data.scene_index if "scene_index" in data else -1 var scene_index = data.scene_index if "scene_index" in data else -1
_sync_enemy_spawn.rpc_id(client_peer_id, spawner.name, pos, scene_index) var humanoid_type = data.humanoid_type if "humanoid_type" in data else -1
print("GameWorld: Sent enemy spawn sync to client ", client_peer_id, ": spawner=", spawner.name, " pos=", pos, " scene_index=", scene_index) _sync_enemy_spawn.rpc_id(client_peer_id, spawner.name, pos, scene_index, humanoid_type)
print("GameWorld: Sent enemy spawn sync to client ", client_peer_id, ": spawner=", spawner.name, " pos=", pos, " scene_index=", scene_index, " humanoid_type=", humanoid_type)
func _on_player_disconnected(peer_id: int): func _on_player_disconnected(peer_id: int):
print("GameWorld: Player disconnected - ", peer_id) print("GameWorld: Player disconnected - ", peer_id)
@@ -169,23 +182,53 @@ func _reset_server_players_ready_flag():
print("GameWorld: Reset all_clients_ready for server player ", child.name) print("GameWorld: Reset all_clients_ready for server player ", child.name)
@rpc("authority", "reliable") @rpc("authority", "reliable")
func _sync_enemy_spawn(spawner_name: String, spawn_position: Vector2, scene_index: int = -1): func _sync_smoke_puffs(_spawner_name: String, puff_positions: Array):
# Clients spawn smoke puffs when server tells them to
if not multiplayer.is_server():
var smoke_puff_scene = preload("res://scenes/smoke_puff.tscn")
if not smoke_puff_scene:
return
var entities_node = get_node_or_null("Entities")
if not entities_node:
return
for pos in puff_positions:
if pos is Vector2:
var puff = smoke_puff_scene.instantiate()
if puff:
puff.global_position = pos
puff.z_index = 10
if puff.has_node("Sprite2D"):
puff.get_node("Sprite2D").z_index = 10
entities_node.add_child(puff)
@rpc("authority", "reliable")
func _sync_enemy_spawn(spawner_name: String, spawn_position: Vector2, scene_index: int = -1, humanoid_type: int = -1):
# Clients spawn enemy when server tells them to # Clients spawn enemy when server tells them to
if not multiplayer.is_server(): if not multiplayer.is_server():
print("GameWorld: Client received RPC to spawn enemy at spawner: ", spawner_name, " position: ", spawn_position, " scene_index: ", scene_index) print("GameWorld: Client received RPC to spawn enemy at spawner: ", spawner_name, " position: ", spawn_position, " scene_index: ", scene_index, " humanoid_type: ", humanoid_type)
# Find the spawner node by name (it's a direct child of GameWorld) # Find the spawner node by name (spawners are children of Entities node, not GameWorld)
var spawner = get_node_or_null(spawner_name) var spawner = null
var entities_node = get_node_or_null("Entities")
if entities_node:
spawner = entities_node.get_node_or_null(spawner_name)
# Fallback: try as direct child of GameWorld (for backwards compatibility)
if not spawner: if not spawner:
push_error("ERROR: Could not find spawner with name: ", spawner_name) spawner = get_node_or_null(spawner_name)
if not spawner:
push_error("ERROR: Could not find spawner with name: ", spawner_name, " in Entities node or GameWorld")
return return
if not spawner.has_method("spawn_enemy_at_position"): if not spawner.has_method("spawn_enemy_at_position"):
push_error("ERROR: Spawner does not have spawn_enemy_at_position method!") push_error("ERROR: Spawner does not have spawn_enemy_at_position method!")
return return
# Call spawn method on the spawner with scene index # Call spawn method on the spawner with scene index and humanoid_type
spawner.spawn_enemy_at_position(spawn_position, scene_index) spawner.spawn_enemy_at_position(spawn_position, scene_index, humanoid_type)
# Loot ID counter (server only) # Loot ID counter (server only)
var loot_id_counter: int = 0 var loot_id_counter: int = 0
@@ -334,19 +377,18 @@ func _request_loot_pickup(loot_id: int, loot_position: Vector2, player_peer_id:
print("GameWorld: Could not find loot for pickup request: id=", loot_id, " pos=", loot_position) print("GameWorld: Could not find loot for pickup request: id=", loot_id, " pos=", loot_position)
@rpc("authority", "reliable") @rpc("authority", "reliable")
func _sync_show_level_complete(enemies_defeated: int, times_downed: int, exp_collected: float, coins_collected: int): func _sync_show_level_complete(level_time: float):
# Clients receive level complete UI sync from server # Clients receive level complete UI sync from server
if multiplayer.is_server(): if multiplayer.is_server():
return # Server ignores this (it's the sender) return # Server ignores this (it's the sender)
# Update stats before showing # Stop HUD timer when level completes (on clients too)
level_enemies_defeated = enemies_defeated var hud = get_node_or_null("IngameHUD")
level_times_downed = times_downed if hud and hud.has_method("stop_timer"):
level_exp_collected = exp_collected hud.stop_timer()
level_coins_collected = coins_collected
# Show level complete UI # Show level complete UI (each client will show their own local player's stats)
_show_level_complete_ui() _show_level_complete_ui(level_time)
@rpc("authority", "reliable") @rpc("authority", "reliable")
func _sync_hide_level_complete(): func _sync_hide_level_complete():
@@ -501,6 +543,9 @@ func _generate_dungeon():
if multiplayer.has_multiplayer_peer(): if multiplayer.has_multiplayer_peer():
_sync_show_level_number.rpc(current_level) _sync_show_level_number.rpc(current_level)
# Load HUD after dungeon generation completes (non-blocking)
call_deferred("_load_hud")
# Sync dungeon to all clients # Sync dungeon to all clients
if multiplayer.has_multiplayer_peer(): if multiplayer.has_multiplayer_peer():
# Get host's current room for spawning new players near host # Get host's current room for spawning new players near host
@@ -514,6 +559,8 @@ func _generate_dungeon():
_sync_dungeon.rpc(dungeon_data, dungeon_seed, current_level, host_room) _sync_dungeon.rpc(dungeon_data, dungeon_seed, current_level, host_room)
print("GameWorld: Dungeon generation completed successfully")
func _render_dungeon(): func _render_dungeon():
if dungeon_data.is_empty(): if dungeon_data.is_empty():
push_error("ERROR: Cannot render dungeon - no dungeon data!") push_error("ERROR: Cannot render dungeon - no dungeon data!")
@@ -1083,6 +1130,10 @@ func _spawn_enemies():
if "damage" in enemy_data: if "damage" in enemy_data:
enemy.damage = enemy_data.damage enemy.damage = enemy_data.damage
# If it's a humanoid enemy, set the humanoid_type
if enemy_type.ends_with("enemy_humanoid.tscn") and "humanoid_type" in enemy_data:
enemy.humanoid_type = enemy_data.humanoid_type
# CRITICAL: Set collision mask BEFORE adding to scene to ensure enemies collide with walls (layer 7 = bit 6 = 64) # CRITICAL: Set collision mask BEFORE adding to scene to ensure enemies collide with walls (layer 7 = bit 6 = 64)
# This overrides any collision_mask set in the scene file # This overrides any collision_mask set in the scene file
enemy.collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64) enemy.collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64)
@@ -1156,7 +1207,7 @@ func _spawn_interactable_objects():
print("GameWorld: Spawning ", objects.size(), " interactable objects (is_server: ", is_server, ")") print("GameWorld: Spawning ", objects.size(), " interactable objects (is_server: ", is_server, ")")
var interactable_object_scene = preload("res://scenes/interactable_object.tscn") var interactable_object_scene = load("res://scenes/interactable_object.tscn")
if not interactable_object_scene: if not interactable_object_scene:
push_error("ERROR: Could not load interactable_object scene!") push_error("ERROR: Could not load interactable_object scene!")
return return
@@ -1290,7 +1341,10 @@ func _sync_dungeon_enemy_spawn(enemy_data: Dictionary):
# Set enemy stats BEFORE adding to scene # Set enemy stats BEFORE adding to scene
if "max_health" in enemy_data: if "max_health" in enemy_data:
enemy.max_health = enemy_data.max_health enemy.max_health = enemy_data.max_health
enemy.current_health = enemy_data.max_health
# If it's a humanoid enemy, set the humanoid_type
if enemy_type.ends_with("enemy_humanoid.tscn") and "humanoid_type" in enemy_data:
enemy.humanoid_type = enemy_data.humanoid_type
if "move_speed" in enemy_data: if "move_speed" in enemy_data:
enemy.move_speed = enemy_data.move_speed enemy.move_speed = enemy_data.move_speed
if "damage" in enemy_data: if "damage" in enemy_data:
@@ -1319,6 +1373,42 @@ func _sync_dungeon_enemy_spawn(enemy_data: Dictionary):
print("GameWorld: Client spawned dungeon enemy via RPC: ", enemy.name, " at ", enemy_data.position, " (type: ", enemy_type, ") authority: ", enemy.get_multiplayer_authority()) print("GameWorld: Client spawned dungeon enemy via RPC: ", enemy.name, " at ", enemy_data.position, " (type: ", enemy_type, ") authority: ", enemy.get_multiplayer_authority())
func _sync_existing_chest_states_to_client(client_peer_id: int):
# Sync chest open states to new client
var entities_node = get_node_or_null("Entities")
if not entities_node:
return
var opened_chest_count = 0
for child in entities_node.get_children():
if child.is_in_group("interactable_object"):
if "object_type" in child and child.object_type == "Chest":
if "is_chest_opened" in child and child.is_chest_opened:
# Found an opened chest - sync it to the client
var obj_name = child.name
_sync_chest_state.rpc_id(client_peer_id, obj_name, true)
opened_chest_count += 1
print("GameWorld: Syncing opened chest ", obj_name, " to client ", client_peer_id)
print("GameWorld: Synced ", opened_chest_count, " opened chests to client ", client_peer_id)
@rpc("authority", "reliable")
func _sync_chest_state(obj_name: String, is_opened: bool):
# Client receives chest state sync
if not multiplayer.is_server():
var entities_node = get_node_or_null("Entities")
if entities_node:
var chest = entities_node.get_node_or_null(obj_name)
if chest and "is_chest_opened" in chest:
chest.is_chest_opened = is_opened
if chest.has_method("_sync_chest_open"):
# Call the sync function to update visuals (loot type doesn't matter for visual sync)
chest._sync_chest_open("coin")
elif "sprite" in chest and "chest_opened_frame" in chest:
if chest.sprite and chest.chest_opened_frame >= 0:
chest.sprite.frame = chest.chest_opened_frame
print("GameWorld: Client received chest state sync for ", obj_name, " - opened: ", is_opened)
func _sync_existing_torches_to_client(client_peer_id: int): func _sync_existing_torches_to_client(client_peer_id: int):
# Sync existing torches to newly connected client # Sync existing torches to newly connected client
if dungeon_data.is_empty() or not dungeon_data.has("torches"): if dungeon_data.is_empty() or not dungeon_data.has("torches"):
@@ -1461,17 +1551,23 @@ func _on_player_reached_stairs(player: Node):
if child.is_in_group("player") and child.has_method("_force_drop_held_object"): if child.is_in_group("player") and child.has_method("_force_drop_held_object"):
child._force_drop_held_object() child._force_drop_held_object()
# Collect stats from all players # Stop HUD timer when level completes
_collect_level_stats() var hud = get_node_or_null("IngameHUD")
var level_time: float = 0.0
if hud and hud.has_method("stop_timer"):
hud.stop_timer()
# Get the level time before stopping
if hud.has_method("get_level_time"):
level_time = hud.get_level_time()
# Fade out player # Fade out player
_fade_out_player(player) _fade_out_player(player)
# Show level complete UI (server and clients) # Show level complete UI (server and clients) with per-player stats
_show_level_complete_ui() _show_level_complete_ui(level_time)
# Sync to all clients # Sync to all clients (each client will show their own local player's stats)
if multiplayer.has_multiplayer_peer(): if multiplayer.has_multiplayer_peer():
_sync_show_level_complete.rpc(level_enemies_defeated, level_times_downed, level_exp_collected, level_coins_collected) _sync_show_level_complete.rpc(level_time)
# After delay, hide UI and generate new level # After delay, hide UI and generate new level
await get_tree().create_timer(5.0).timeout # Show stats for 5 seconds await get_tree().create_timer(5.0).timeout # Show stats for 5 seconds
@@ -1515,6 +1611,11 @@ func _on_player_reached_stairs(player: Node):
print("GameWorld: Syncing level number ", current_level, " to all clients") print("GameWorld: Syncing level number ", current_level, " to all clients")
_sync_show_level_number.rpc(current_level) _sync_show_level_number.rpc(current_level)
# Restart HUD timer for new level
hud = get_node_or_null("IngameHUD")
if hud and hud.has_method("start_timer"):
hud.start_timer()
# Move all players to start room (server side) # Move all players to start room (server side)
_move_all_players_to_start_room() _move_all_players_to_start_room()
@@ -1537,32 +1638,40 @@ func _on_player_reached_stairs(player: Node):
_sync_dungeon.rpc(dungeon_data, dungeon_seed, current_level, start_room) _sync_dungeon.rpc(dungeon_data, dungeon_seed, current_level, start_room)
func _collect_level_stats(): func _get_local_player_stats() -> Dictionary:
# Reset stats # Get stats for the local player (for level complete screen)
level_enemies_defeated = 0 # Returns a dictionary with: enemies_defeated, coins_collected
level_times_downed = 0 var stats = {
level_exp_collected = 0.0 "enemies_defeated": 0,
level_coins_collected = 0 "coins_collected": 0
}
# Collect from all players # Find local player(s) - in multiplayer, find player with authority matching this client
# In single-player, just use the first player
var players = get_tree().get_nodes_in_group("player") var players = get_tree().get_nodes_in_group("player")
var local_player = null
if multiplayer.has_multiplayer_peer():
# Multiplayer: find player with matching authority
for player in players: for player in players:
if player.character_stats: if player.has_method("is_multiplayer_authority") and player.is_multiplayer_authority():
# Count enemies defeated (kills) local_player = player
if "kills" in player.character_stats: break
level_enemies_defeated += player.character_stats.kills else:
# Single-player: use first player
if players.size() > 0:
local_player = players[0]
# Count times downed (deaths) if local_player and local_player.character_stats:
if "deaths" in player.character_stats: # Get enemies defeated (kills)
level_times_downed += player.character_stats.deaths if "kills" in local_player.character_stats:
stats.enemies_defeated = local_player.character_stats.kills
# Collect exp # Get coins collected
if "xp" in player.character_stats: if "coin" in local_player.character_stats:
level_exp_collected += player.character_stats.xp stats.coins_collected = local_player.character_stats.coin
# Collect coins return stats
if "coin" in player.character_stats:
level_coins_collected += player.character_stats.coin
func _fade_out_player(player: Node): func _fade_out_player(player: Node):
# Fade out all sprite layers # Fade out all sprite layers
@@ -1637,7 +1746,7 @@ func _fade_in_player(player: Node):
sprite_layer.modulate.a = 0.0 # Start invisible sprite_layer.modulate.a = 0.0 # Start invisible
fade_tween.tween_property(sprite_layer, "modulate:a", 1.0, 1.0) fade_tween.tween_property(sprite_layer, "modulate:a", 1.0, 1.0)
func _show_level_complete_ui(): func _show_level_complete_ui(level_time: float = 0.0):
# Create or show level complete UI # Create or show level complete UI
var level_complete_ui = get_node_or_null("LevelCompleteUI") var level_complete_ui = get_node_or_null("LevelCompleteUI")
if not level_complete_ui: if not level_complete_ui:
@@ -1659,11 +1768,17 @@ func _show_level_complete_ui():
if level_complete_ui: if level_complete_ui:
if level_complete_ui.has_method("show_stats"): if level_complete_ui.has_method("show_stats"):
# Get stats for local player
var local_stats = _get_local_player_stats()
# Use current_level directly (matches what HUD shows)
# current_level hasn't been incremented yet when this is called
level_complete_ui.show_stats( level_complete_ui.show_stats(
level_enemies_defeated, local_stats.enemies_defeated,
level_times_downed, 0, # times_downed - not shown per user request
level_exp_collected, 0.0, # exp_collected - not shown per user request
level_coins_collected local_stats.coins_collected,
level_time,
current_level
) )
func _show_level_number(): func _show_level_number():
@@ -1699,47 +1814,167 @@ func _show_level_number():
else: else:
print("GameWorld: ERROR - Could not create or find LevelTextUI!") print("GameWorld: ERROR - Could not create or find LevelTextUI!")
func _load_hud():
# Check if HUD already exists - only load it once
var existing_hud = get_node_or_null("IngameHUD")
if existing_hud and is_instance_valid(existing_hud):
print("GameWorld: HUD already exists, skipping load (will just reset timer)")
# Reset timer for new level if method exists
if existing_hud.has_method("reset_level_timer"):
existing_hud.reset_level_timer()
return
# Load HUD dynamically to avoid scene loading errors
# This is optional - don't crash if HUD scene doesn't exist or fails to load
# Use a try-catch-like approach by checking for errors
var hud_scene_path = "res://scenes/ingame_hud.tscn"
# Check if scene exists
if not ResourceLoader.exists(hud_scene_path):
print("GameWorld: HUD scene not found at ", hud_scene_path, " - HUD disabled")
return
# Try to load the scene
var hud_scene = load(hud_scene_path)
if not hud_scene:
print("GameWorld: Warning - Failed to load HUD scene from ", hud_scene_path)
return
# Try to instantiate
var hud = null
if hud_scene.has_method("instantiate"):
hud = hud_scene.instantiate()
else:
print("GameWorld: Warning - HUD scene is not a PackedScene")
return
if not hud:
print("GameWorld: Warning - Failed to instantiate HUD scene")
return
# Add to scene tree
hud.name = "IngameHUD"
# Ensure HUD is visible and on top layer
hud.visible = true
hud.layer = 100 # High layer to ensure HUD is on top of everything
add_child(hud)
# Reset timer if method exists
if hud.has_method("reset_level_timer"):
hud.reset_level_timer()
print("GameWorld: HUD loaded successfully and added to scene tree")
print("GameWorld: HUD visible: ", hud.visible, ", layer: ", hud.layer)
func _initialize_hud():
# Find or get the HUD and reset its level timer
# This is optional - don't crash if HUD doesn't exist
var hud = get_node_or_null("IngameHUD")
if hud and is_instance_valid(hud) and hud.has_method("reset_level_timer"):
hud.reset_level_timer()
else:
print("GameWorld: HUD not found or not ready - this is OK if HUD scene is missing")
func _create_level_complete_ui_programmatically() -> Node: func _create_level_complete_ui_programmatically() -> Node:
# Create level complete UI programmatically # Create level complete UI programmatically
var canvas_layer = CanvasLayer.new() var canvas_layer = CanvasLayer.new()
canvas_layer.name = "LevelCompleteUI" canvas_layer.name = "LevelCompleteUI"
add_child(canvas_layer) add_child(canvas_layer)
# Load standard font (as FontFile)
var standard_font = load("res://assets/fonts/standard_font.png") as FontFile
if not standard_font:
print("GameWorld: Warning - Could not load standard_font.png as FontFile")
# Create theme with standard font
var theme = Theme.new()
if standard_font:
theme.default_font = standard_font
theme.default_font_size = 10
var vbox = VBoxContainer.new() var vbox = VBoxContainer.new()
vbox.set_anchors_preset(Control.PRESET_CENTER) vbox.theme = theme
vbox.offset_top = -200 # Position a bit up from center
canvas_layer.add_child(vbox) canvas_layer.add_child(vbox)
# Title # Center the VBoxContainer properly
# Use PRESET_CENTER to anchor at center, then set offsets to center horizontally
var screen_size = get_viewport().get_visible_rect().size
vbox.set_anchors_preset(Control.PRESET_CENTER)
# Set offsets so container is centered horizontally (equal left/right offsets)
# and positioned vertically (offset from center)
vbox.offset_left = - screen_size.x / 2
vbox.offset_right = screen_size.x / 2
vbox.offset_top = -200 # Position a bit up from center
vbox.offset_bottom = screen_size.y / 2 - 200 # Balance to maintain vertical position
vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
# Title - "LEVEL COMPLETE!" in large size
var title = Label.new() var title = Label.new()
title.name = "TitleLabel"
title.text = "LEVEL COMPLETE!" title.text = "LEVEL COMPLETE!"
title.add_theme_font_size_override("font_size", 48) title.theme = theme
title.add_theme_font_size_override("font_size", 72) # Large size
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
vbox.add_child(title) vbox.add_child(title)
# Stats header - "stats" in smaller size
var stats_header = Label.new()
stats_header.name = "StatsHeaderLabel"
stats_header.text = "stats"
stats_header.theme = theme
stats_header.add_theme_font_size_override("font_size", 32) # Smaller than title
stats_header.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
stats_header.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
vbox.add_child(stats_header)
# Stats container # Stats container
var stats_container = VBoxContainer.new() var stats_container = VBoxContainer.new()
stats_container.theme = theme
stats_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
vbox.add_child(stats_container) vbox.add_child(stats_container)
# Stats labels # Stats labels - enemies defeated and level time
var enemies_label = Label.new() var enemies_label = Label.new()
enemies_label.name = "EnemiesLabel" enemies_label.name = "EnemiesLabel"
enemies_label.theme = theme
enemies_label.add_theme_font_size_override("font_size", 24) enemies_label.add_theme_font_size_override("font_size", 24)
enemies_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
enemies_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
stats_container.add_child(enemies_label) stats_container.add_child(enemies_label)
var time_label = Label.new()
time_label.name = "TimeLabel"
time_label.theme = theme
time_label.add_theme_font_size_override("font_size", 24)
time_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
time_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
stats_container.add_child(time_label)
var downed_label = Label.new() var downed_label = Label.new()
downed_label.name = "DownedLabel" downed_label.name = "DownedLabel"
downed_label.theme = theme
downed_label.add_theme_font_size_override("font_size", 24) downed_label.add_theme_font_size_override("font_size", 24)
downed_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
downed_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
stats_container.add_child(downed_label) stats_container.add_child(downed_label)
var exp_label = Label.new() var exp_label = Label.new()
exp_label.name = "ExpLabel" exp_label.name = "ExpLabel"
exp_label.theme = theme
exp_label.add_theme_font_size_override("font_size", 24) exp_label.add_theme_font_size_override("font_size", 24)
exp_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
exp_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
stats_container.add_child(exp_label) stats_container.add_child(exp_label)
var coins_label = Label.new() var coins_label = Label.new()
coins_label.name = "CoinsLabel" coins_label.name = "CoinsLabel"
coins_label.theme = theme
coins_label.add_theme_font_size_override("font_size", 24) coins_label.add_theme_font_size_override("font_size", 24)
coins_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
coins_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
stats_container.add_child(coins_label) stats_container.add_child(coins_label)
# Add script # Add script
@@ -1810,7 +2045,7 @@ func _spawn_blocking_doors():
if blocking_doors == null or not blocking_doors is Array: if blocking_doors == null or not blocking_doors is Array:
return return
var door_scene = preload("res://scenes/door.tscn") var door_scene = load("res://scenes/door.tscn")
if not door_scene: if not door_scene:
push_error("ERROR: Could not load door scene!") push_error("ERROR: Could not load door scene!")
return return
@@ -1822,6 +2057,9 @@ func _spawn_blocking_doors():
print("GameWorld: Spawning ", blocking_doors.size(), " blocking doors") print("GameWorld: Spawning ", blocking_doors.size(), " blocking doors")
# Track pillar placement per room to avoid duplicates
var rooms_with_pillars: Dictionary = {} # Key: room string "x,y", Value: true if pillar exists
for i in range(blocking_doors.size()): for i in range(blocking_doors.size()):
var door_data = blocking_doors[i] var door_data = blocking_doors[i]
if not door_data is Dictionary: if not door_data is Dictionary:
@@ -1864,14 +2102,24 @@ func _spawn_blocking_doors():
print("GameWorld: Creating blocking door ", door.name, " (", door_data.type, ") for puzzle room (", door_data.blocking_room.x, ", ", door_data.blocking_room.y, "), puzzle_type: ", door_data.puzzle_type) print("GameWorld: Creating blocking door ", door.name, " (", door_data.type, ") for puzzle room (", door_data.blocking_room.x, ", ", door_data.blocking_room.y, "), puzzle_type: ", door_data.puzzle_type)
# CRITICAL: Store original door connection info from door_data.door # CRITICAL: Store original door connection info from door_data
# For blocking doors: room1 = puzzle room (where door is IN / leads FROM) # For blocking doors: room1 = puzzle room (where door is IN / leads FROM)
# room2 = other room (where door leads TO) # room2 = other room (where door leads TO)
# blocking_room = puzzle room (same as room1, where puzzle is) # blocking_room = puzzle room (same as room1, where puzzle is)
if "door" in door_data and door_data.door is Dictionary: # Use original_room1 and original_room2 from door_data (which may have been swapped)
if "original_room1" in door_data and door_data.original_room1:
door.room1 = door_data.original_room1 # This is always the puzzle room
elif "door" in door_data and door_data.door is Dictionary:
# Fallback to original door if original_room1 not set
var original_door = door_data.door var original_door = door_data.door
if "room1" in original_door and original_door.room1: if "room1" in original_door and original_door.room1:
door.room1 = original_door.room1 door.room1 = original_door.room1
if "original_room2" in door_data and door_data.original_room2:
door.room2 = door_data.original_room2 # This is always the other room
elif "door" in door_data and door_data.door is Dictionary:
# Fallback to original door if original_room2 not set
var original_door = door_data.door
if "room2" in original_door and original_door.room2: if "room2" in original_door and original_door.room2:
door.room2 = original_door.room2 door.room2 = original_door.room2
@@ -1990,6 +2238,42 @@ func _spawn_blocking_doors():
door.connected_switches.append(existing_switch) door.connected_switches.append(existing_switch)
has_puzzle_element = true has_puzzle_element = true
print("GameWorld: Connected door ", door.name, " (room: ", door_blocking_room.get("x", "?"), ",", door_blocking_room.get("y", "?"), ") to existing switch ", existing_switch.name, " in SAME room") print("GameWorld: Connected door ", door.name, " (room: ", door_blocking_room.get("x", "?"), ",", door_blocking_room.get("y", "?"), ") to existing switch ", existing_switch.name, " in SAME room")
# If this is a pillar switch, ensure a pillar exists in the room
# Check if switch is a pillar switch (check both door_data and existing switch)
var is_pillar_switch = (switch_type == "pillar")
if not is_pillar_switch and "switch_type" in existing_switch:
is_pillar_switch = (existing_switch.switch_type == "pillar")
if is_pillar_switch:
# Check if we've already verified/placed a pillar for this room
var room_key = str(door_blocking_room.x) + "," + str(door_blocking_room.y)
if not rooms_with_pillars.has(room_key):
# First time checking this room - check if pillar exists
var pillar_exists_in_room = false
for obj in get_tree().get_nodes_in_group("interactable_object"):
if is_instance_valid(obj):
var obj_type = obj.object_type if "object_type" in obj else ""
if obj_type == "Pillar":
if obj.has_meta("room"):
var obj_room = obj.get_meta("room")
if obj_room and not obj_room.is_empty():
if obj_room.x == door_blocking_room.x and obj_room.y == door_blocking_room.y and \
obj_room.w == door_blocking_room.w and obj_room.h == door_blocking_room.h:
pillar_exists_in_room = true
print("GameWorld: Found existing pillar in room (", door_blocking_room.x, ",", door_blocking_room.y, ")")
break
# If no pillar exists, place one
if not pillar_exists_in_room:
print("GameWorld: Pillar switch found but no pillar in room (", door_blocking_room.x, ",", door_blocking_room.y, ") - placing pillar now")
_place_pillar_in_room(door_blocking_room, existing_switch.global_position)
# Mark room as checked after attempting to place pillar
# Note: Even if placement fails, mark as checked to avoid repeated attempts
rooms_with_pillars[room_key] = true
else:
# Pillar exists - mark room as checked
rooms_with_pillars[room_key] = true
else: else:
push_error("GameWorld: ERROR - Attempted to connect door ", door.name, " to switch ", existing_switch.name, " but rooms don't match! Door room: (", door_blocking_room.get("x", "?"), ",", door_blocking_room.get("y", "?"), "), Switch room: (", existing_switch_room_final.get("x", "?") if existing_switch_room_final else "?", ",", existing_switch_room_final.get("y", "?") if existing_switch_room_final else "?", ")") push_error("GameWorld: ERROR - Attempted to connect door ", door.name, " to switch ", existing_switch.name, " but rooms don't match! Door room: (", door_blocking_room.get("x", "?"), ",", door_blocking_room.get("y", "?"), "), Switch room: (", existing_switch_room_final.get("x", "?") if existing_switch_room_final else "?", ",", existing_switch_room_final.get("y", "?") if existing_switch_room_final else "?", ")")
# Don't connect - spawn a new switch instead # Don't connect - spawn a new switch instead
@@ -2038,7 +2322,16 @@ func _spawn_blocking_doors():
# If this is a pillar switch, place a pillar in the same room # If this is a pillar switch, place a pillar in the same room
if switch_type == "pillar": if switch_type == "pillar":
# Check if we've already placed a pillar for this room
var room_key = str(door_blocking_room.x) + "," + str(door_blocking_room.y)
if not rooms_with_pillars.has(room_key):
print("GameWorld: Placing pillar for new pillar switch in room (", door_blocking_room.x, ",", door_blocking_room.y, ")")
_place_pillar_in_room(door_blocking_room, switch_pos) _place_pillar_in_room(door_blocking_room, switch_pos)
# Mark room as checked after attempting to place pillar
# Note: Even if placement fails, mark as checked to avoid repeated attempts
rooms_with_pillars[room_key] = true
else:
print("GameWorld: Pillar already exists/placed for room (", door_blocking_room.x, ",", door_blocking_room.y, ") - skipping placement")
else: else:
push_error("GameWorld: ERROR - Switch ", switch.name, " room (", switch_room_check.x, ",", switch_room_check.y, ") doesn't match door blocking_room (", door_blocking_room.x, ",", door_blocking_room.y, ")! NOT connecting! Removing switch.") push_error("GameWorld: ERROR - Switch ", switch.name, " room (", switch_room_check.x, ",", switch_room_check.y, ") doesn't match door blocking_room (", door_blocking_room.x, ",", door_blocking_room.y, ")! NOT connecting! Removing switch.")
switch.queue_free() # Remove the switch since it's in wrong room switch.queue_free() # Remove the switch since it's in wrong room
@@ -2119,15 +2412,18 @@ func _spawn_blocking_doors():
print("GameWorld: Spawned ", blocking_doors.size(), " blocking doors") print("GameWorld: Spawned ", blocking_doors.size(), " blocking doors")
func _spawn_floor_switch(i_position: Vector2, required_weight: float, tile_x: int, tile_y: int, switch_type: String = "walk", switch_room: Dictionary = {}) -> Node: func _spawn_floor_switch(i_position: Vector2, required_weight: float, tile_x: int, tile_y: int, switch_type: String = "walk", switch_room: Dictionary = {}) -> Node:
# Spawn a floor switch # Spawn a floor switch using the scene file
# switch_type: "walk" for walk-on switch (weight 1), "pillar" for pillar switch (weight 5) # switch_type: "walk" for walk-on switch (weight 1), "pillar" for pillar switch (weight 5)
var switch_script = load("res://scripts/floor_switch.gd") var switch_scene = preload("res://scenes/floor_switch.tscn")
if not switch_script: if not switch_scene:
push_error("ERROR: Could not load floor_switch script!") push_error("ERROR: Could not load floor_switch scene!")
return null
var switch = switch_scene.instantiate()
if not switch:
push_error("ERROR: Could not instantiate floor_switch scene!")
return null return null
var switch = Area2D.new()
switch.set_script(switch_script)
switch.name = "FloorSwitch_%d_%d" % [tile_x, tile_y] switch.name = "FloorSwitch_%d_%d" % [tile_x, tile_y]
switch.add_to_group("floor_switch") switch.add_to_group("floor_switch")
@@ -2136,13 +2432,6 @@ func _spawn_floor_switch(i_position: Vector2, required_weight: float, tile_x: in
switch.required_weight = required_weight # Will be overridden in _ready() based on switch_type, but set it here too switch.required_weight = required_weight # Will be overridden in _ready() based on switch_type, but set it here too
switch.switch_tile_position = Vector2i(tile_x, tile_y) switch.switch_tile_position = Vector2i(tile_x, tile_y)
# Create collision shape
var collision_shape = CollisionShape2D.new()
var circle_shape = CircleShape2D.new()
circle_shape.radius = 8.0 # 16 pixel diameter
collision_shape.shape = circle_shape
switch.add_child(collision_shape)
# Set multiplayer authority # Set multiplayer authority
if multiplayer.has_multiplayer_peer(): if multiplayer.has_multiplayer_peer():
switch.set_multiplayer_authority(1) switch.set_multiplayer_authority(1)
@@ -2259,9 +2548,11 @@ func _spawn_room_triggers():
print("GameWorld: Spawning ", rooms.size(), " room triggers") print("GameWorld: Spawning ", rooms.size(), " room triggers")
var triggers_spawned = 0
for i in range(rooms.size()): for i in range(rooms.size()):
var room = rooms[i] var room = rooms[i]
if not room is Dictionary: if not room is Dictionary:
print("GameWorld: WARNING - Room at index ", i, " is not a Dictionary, skipping")
continue continue
var trigger = Area2D.new() var trigger = Area2D.new()
@@ -2298,8 +2589,10 @@ func _spawn_room_triggers():
# Add to scene # Add to scene
entities_node.add_child(trigger) entities_node.add_child(trigger)
triggers_spawned += 1
print("GameWorld: Added room trigger ", trigger.name, " for room (", room.x, ", ", room.y, ") - ", triggers_spawned, "/", rooms.size())
print("GameWorld: Spawned ", rooms.size(), " room triggers") print("GameWorld: Spawned ", triggers_spawned, " room triggers (out of ", rooms.size(), " rooms)")
func _place_key_in_room(room: Dictionary): func _place_key_in_room(room: Dictionary):
# Place a key in the specified room (as loot) # Place a key in the specified room (as loot)
@@ -2352,7 +2645,7 @@ func _place_pillar_in_room(room: Dictionary, switch_position: Vector2):
if room.is_empty(): if room.is_empty():
return return
var interactable_object_scene = preload("res://scenes/interactable_object.tscn") var interactable_object_scene = load("res://scenes/interactable_object.tscn")
if not interactable_object_scene: if not interactable_object_scene:
push_error("ERROR: Could not load interactable_object scene for pillar!") push_error("ERROR: Could not load interactable_object scene for pillar!")
return return
@@ -2366,7 +2659,8 @@ func _place_pillar_in_room(room: Dictionary, switch_position: Vector2):
var tile_size = 16 var tile_size = 16
var valid_positions = [] var valid_positions = []
# Room interior is from room.x + 2 to room.x + room.w - 2 (excluding 2-tile walls) # Room interior floor tiles: from room.x + 2 to room.x + room.w - 3 (excluding 2-tile walls on each side)
# Floor tiles are at: room.x+2, room.x+3, ..., room.x+room.w-3 (last floor tile before right wall)
# CRITICAL: Also exclude last row/column of floor tiles to prevent objects from spawning in walls # CRITICAL: Also exclude last row/column of floor tiles to prevent objects from spawning in walls
# Objects are 16x16, so we need at least 1 tile buffer from walls # Objects are 16x16, so we need at least 1 tile buffer from walls
# Floor tiles are at: room.x+2, room.x+3, ..., room.x+room.w-3 (last floor tile before wall) # Floor tiles are at: room.x+2, room.x+3, ..., room.x+room.w-3 (last floor tile before wall)
@@ -2376,6 +2670,12 @@ func _place_pillar_in_room(room: Dictionary, switch_position: Vector2):
var min_y = room.y + 2 var min_y = room.y + 2
var max_y = room.y + room.h - 4 # Exclude last 2 floor tiles (room.h-3 is last floor, room.h-4 is second-to-last) var max_y = room.y + room.h - 4 # Exclude last 2 floor tiles (room.h-3 is last floor, room.h-4 is second-to-last)
var interior_width = room.w - 4 # Exclude 2-tile walls on each side
var interior_height = room.h - 4 # Exclude 2-tile walls on each side
print("GameWorld: _place_pillar_in_room - Searching for valid positions in room (", room.x, ",", room.y, ",", room.w, "x", room.h, ")")
print("GameWorld: Interior size: ", interior_width, "x", interior_height, ", Checking tiles: x[", min_x, " to ", max_x, "], y[", min_y, " to ", max_y, "]")
print("GameWorld: Switch position: ", switch_position)
for x in range(min_x, max_x + 1): # +1 because range is exclusive at end for x in range(min_x, max_x + 1): # +1 because range is exclusive at end
for y in range(min_y, max_y + 1): for y in range(min_y, max_y + 1):
if x >= 0 and x < dungeon_data.map_size.x and y >= 0 and y < dungeon_data.map_size.y: if x >= 0 and x < dungeon_data.map_size.x and y >= 0 and y < dungeon_data.map_size.y:
@@ -2388,15 +2688,22 @@ func _place_pillar_in_room(room: Dictionary, switch_position: Vector2):
var world_y = y * tile_size + 8 var world_y = y * tile_size + 8
var world_pos = Vector2(world_x, world_y) var world_pos = Vector2(world_x, world_y)
# Ensure pillar is at least 2 tiles away from the switch # Ensure pillar is at least 1 tile away from the switch
var distance_to_switch = world_pos.distance_to(switch_position) var distance_to_switch = world_pos.distance_to(switch_position)
if distance_to_switch >= tile_size * 2: # At least 2 tiles away if distance_to_switch >= tile_size * 1: # At least 1 tiles away
valid_positions.append(world_pos) valid_positions.append(world_pos)
print("GameWorld: Valid position found at (", x, ",", y, ") -> world (", world_x, ",", world_y, "), distance to switch: ", distance_to_switch)
else:
print("GameWorld: Position at (", x, ",", y, ") -> world (", world_x, ",", world_y, ") too close to switch (distance: ", distance_to_switch, " < ", tile_size, ")")
print("GameWorld: Found ", valid_positions.size(), " valid positions for pillar")
if valid_positions.size() > 0: if valid_positions.size() > 0:
# Pick a random position # Pick a deterministic random position using dungeon seed
# This ensures server and clients place pillars in the same positions
var rng = RandomNumberGenerator.new() var rng = RandomNumberGenerator.new()
rng.randomize() # Use dungeon seed + room position + switch position as seed for deterministic randomness
var pillar_seed = dungeon_seed + (room.x * 1000) + (room.y * 100) + int(switch_position.x) + int(switch_position.y)
rng.seed = pillar_seed
var pillar_pos = valid_positions[rng.randi() % valid_positions.size()] var pillar_pos = valid_positions[rng.randi() % valid_positions.size()]
# Spawn pillar interactable object # Spawn pillar interactable object

View File

@@ -271,6 +271,13 @@ func _break_into_pieces():
get_parent().call_deferred("add_child", tp) get_parent().call_deferred("add_child", tp)
play_destroy_sound()
self.set_deferred("collision_layer", 0)
self.visible = false
if ($SfxShatter.playing):
await $SfxShatter.finished
if ($SfxBreakCrate.playing):
await $SfxShatter.finished
# Remove self # Remove self
queue_free() queue_free()
@@ -294,13 +301,14 @@ func on_grabbed(by_player):
# In multiplayer, send RPC to server if client is opening # In multiplayer, send RPC to server if client is opening
if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority(): if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority():
# Client - send request to server # Client - send request to server
if by_player and by_player.is_multiplayer_authority(): if by_player:
var player_peer_id = by_player.get_multiplayer_authority() var player_peer_id = by_player.get_multiplayer_authority()
print("Chest: Client sending RPC to open chest, player_peer_id: ", player_peer_id)
_request_chest_open.rpc_id(1, player_peer_id) _request_chest_open.rpc_id(1, player_peer_id)
else: else:
# Server or single player - open directly # Server or single player - open directly
_open_chest(by_player) _open_chest(by_player)
return return # CRITICAL: Return early to prevent normal grab behavior
is_being_held = true is_being_held = true
held_by_player = by_player held_by_player = by_player
@@ -434,7 +442,20 @@ func setup_chest():
var chest_frames = [12, 31] var chest_frames = [12, 31]
var opened_frames = [13, 32] var opened_frames = [13, 32]
var index = randi() % chest_frames.size()
# Use deterministic randomness based on dungeon seed and position
# This ensures host and clients get the same chest variant
var chest_seed = 0
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and "dungeon_seed" in game_world:
chest_seed = game_world.dungeon_seed
# Add position to seed to make each chest unique but deterministic
chest_seed += int(global_position.x) * 1000 + int(global_position.y)
var rng = RandomNumberGenerator.new()
rng.seed = chest_seed
var index = rng.randi() % chest_frames.size()
chest_closed_frame = chest_frames[index] chest_closed_frame = chest_frames[index]
chest_opened_frame = opened_frames[index] chest_opened_frame = opened_frames[index]
@@ -478,7 +499,7 @@ func _open_chest(by_player: Node = null):
# Only process on server (authority) # Only process on server (authority)
if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority(): if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority():
return return
$SfxOpenChest.play()
is_chest_opened = true is_chest_opened = true
if sprite and chest_opened_frame >= 0: if sprite and chest_opened_frame >= 0:
sprite.frame = chest_opened_frame sprite.frame = chest_opened_frame
@@ -502,34 +523,34 @@ func _open_chest(by_player: Node = null):
by_player.add_coins(1) by_player.add_coins(1)
# Show pickup notification with coin graphic # Show pickup notification with coin graphic
var coin_texture = load("res://assets/gfx/pickups/gold_coin.png") var coin_texture = load("res://assets/gfx/pickups/gold_coin.png")
_show_item_pickup_notification(by_player, "+1 coin", selected_loot.color, coin_texture, 6, 1, 0) _show_item_pickup_notification(by_player, "+1 COIN", selected_loot.color, coin_texture, 6, 1, 0)
"apple": "apple":
var heal_amount = 20.0 var heal_amount = 20.0
if by_player.has_method("heal"): if by_player.has_method("heal"):
by_player.heal(heal_amount) by_player.heal(heal_amount)
# Show pickup notification with apple graphic # Show pickup notification with apple graphic
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
_show_item_pickup_notification(by_player, "+" + str(int(heal_amount)) + " hp", selected_loot.color, items_texture, 20, 14, (8 * 20) + 11) _show_item_pickup_notification(by_player, "+" + str(int(heal_amount)) + " HP", selected_loot.color, items_texture, 20, 14, (8 * 20) + 11)
"banana": "banana":
var heal_amount = 20.0 var heal_amount = 20.0
if by_player.has_method("heal"): if by_player.has_method("heal"):
by_player.heal(heal_amount) by_player.heal(heal_amount)
# Show pickup notification with banana graphic # Show pickup notification with banana graphic
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
_show_item_pickup_notification(by_player, "+" + str(int(heal_amount)) + " hp", selected_loot.color, items_texture, 20, 14, (8 * 20) + 12) _show_item_pickup_notification(by_player, "+" + str(int(heal_amount)) + " HP", selected_loot.color, items_texture, 20, 14, (8 * 20) + 12)
"cherry": "cherry":
var heal_amount = 20.0 var heal_amount = 20.0
if by_player.has_method("heal"): if by_player.has_method("heal"):
by_player.heal(heal_amount) by_player.heal(heal_amount)
# Show pickup notification with cherry graphic # Show pickup notification with cherry graphic
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
_show_item_pickup_notification(by_player, "+" + str(int(heal_amount)) + " hp", selected_loot.color, items_texture, 20, 14, (8 * 20) + 13) _show_item_pickup_notification(by_player, "+" + str(int(heal_amount)) + " HP", selected_loot.color, items_texture, 20, 14, (8 * 20) + 13)
"key": "key":
if by_player.has_method("add_key"): if by_player.has_method("add_key"):
by_player.add_key(1) by_player.add_key(1)
# Show pickup notification with key graphic # Show pickup notification with key graphic
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
_show_item_pickup_notification(by_player, "+1 key", selected_loot.color, items_texture, 20, 14, (13 * 20) + 10) _show_item_pickup_notification(by_player, "+1 KEY", selected_loot.color, items_texture, 20, 14, (13 * 20) + 10)
# Play chest open sound # Play chest open sound
if has_node("SfxChestOpen"): if has_node("SfxChestOpen"):
@@ -549,7 +570,10 @@ func _request_chest_open(player_peer_id: int):
if not multiplayer.is_server(): if not multiplayer.is_server():
return return
print("Chest: Server received RPC to open chest, player_peer_id: ", player_peer_id)
if is_chest_opened: if is_chest_opened:
print("Chest: Chest already opened, ignoring request")
return return
# Find the player by peer ID # Find the player by peer ID
@@ -564,6 +588,7 @@ func _request_chest_open(player_peer_id: int):
push_error("Chest: ERROR - Could not find player with peer_id ", player_peer_id, " for chest opening!") push_error("Chest: ERROR - Could not find player with peer_id ", player_peer_id, " for chest opening!")
return return
print("Chest: Found player ", player.name, " for peer_id ", player_peer_id, ", opening chest")
# Open chest on server (this will give item to player) # Open chest on server (this will give item to player)
_open_chest(player) _open_chest(player)
@@ -588,3 +613,24 @@ func _show_item_pickup_notification(player: Node, text: String, text_color: Colo
parent.add_child(floating_text) parent.add_child(floating_text)
floating_text.global_position = player.global_position + Vector2(0, -20) floating_text.global_position = player.global_position + Vector2(0, -20)
floating_text.setup(text, text_color, 0.5, 0.5, item_texture, sprite_hframes, sprite_vframes, sprite_frame) # Show for 0.5s, fade over 0.5s floating_text.setup(text, text_color, 0.5, 0.5, item_texture, sprite_hframes, sprite_vframes, sprite_frame) # Show for 0.5s, fade over 0.5s
func play_destroy_sound():
if object_type == "Pot":
$SfxShatter.play()
else:
$SfxBreakCrate.play()
pass
func play_drag_sound():
if object_type == "Pot":
if !$SfxDrag.playing:
$SfxDrag.play()
else:
if !$SfxDragRock.playing:
$SfxDragRock.play()
pass
func stop_drag_sound():
$SfxDrag.stop()
$SfxDragRock.stop()
pass

View File

@@ -3,21 +3,33 @@ extends CanvasLayer
# Level Complete UI - Shows stats when level is completed # Level Complete UI - Shows stats when level is completed
var title_label: Label = null var title_label: Label = null
var stats_header_label: Label = null
var enemies_label: Label = null var enemies_label: Label = null
var time_label: Label = null
var downed_label: Label = null var downed_label: Label = null
var exp_label: Label = null var exp_label: Label = null
var coins_label: Label = null var coins_label: Label = null
func _ready(): func _ready():
visible = false visible = false
_find_labels()
func _find_labels():
# Find labels (works for both scene-based and programmatically created UI) # Find labels (works for both scene-based and programmatically created UI)
var vbox = get_child(0) if get_child_count() > 0 else null var vbox = get_child(0) if get_child_count() > 0 else null
if vbox: if vbox:
for child in vbox.get_children(): for child in vbox.get_children():
if child is Label and child.text == "LEVEL COMPLETE!": if child is Label:
# Check by name first (more reliable)
if child.name == "TitleLabel" or (title_label == null and (child.text == "LEVEL COMPLETE!" or child.text.begins_with("LEVEL"))):
title_label = child title_label = child
elif child.name == "StatsHeaderLabel" or (stats_header_label == null and child.text == "stats"):
stats_header_label = child
# Check by name for other labels
elif child.name == "EnemiesLabel": elif child.name == "EnemiesLabel":
enemies_label = child enemies_label = child
elif child.name == "TimeLabel":
time_label = child
elif child.name == "DownedLabel": elif child.name == "DownedLabel":
downed_label = child downed_label = child
elif child.name == "ExpLabel": elif child.name == "ExpLabel":
@@ -29,6 +41,8 @@ func _ready():
for stat_child in child.get_children(): for stat_child in child.get_children():
if stat_child.name == "EnemiesLabel": if stat_child.name == "EnemiesLabel":
enemies_label = stat_child enemies_label = stat_child
elif stat_child.name == "TimeLabel":
time_label = stat_child
elif stat_child.name == "DownedLabel": elif stat_child.name == "DownedLabel":
downed_label = stat_child downed_label = stat_child
elif stat_child.name == "ExpLabel": elif stat_child.name == "ExpLabel":
@@ -36,22 +50,48 @@ func _ready():
elif stat_child.name == "CoinsLabel": elif stat_child.name == "CoinsLabel":
coins_label = stat_child coins_label = stat_child
func show_stats(enemies_defeated: int, times_downed: int, exp_collected: float, coins_collected: int): func show_stats(enemies_defeated: int, times_downed: int, exp_collected: float, coins_collected: int, level_time: float, level_number: int = 1):
# Ensure labels are found (in case _ready() hasn't run yet)
_find_labels()
# Update labels # Update labels
if title_label: if title_label:
title_label.text = "LEVEL COMPLETE!" title_label.text = "LEVEL " + str(level_number) + " COMPLETE!"
else:
print("LevelCompleteUI: WARNING - title_label not found!")
if stats_header_label:
stats_header_label.text = "stats"
if enemies_label: if enemies_label:
enemies_label.text = "Enemies Defeated: " + str(enemies_defeated) enemies_label.text = "Enemies defeated: " + str(enemies_defeated)
enemies_label.visible = true
else:
print("LevelCompleteUI: WARNING - enemies_label not found!")
# Format level time as MM:SS
if time_label:
var minutes = int(level_time) / 60
var seconds = int(level_time) % 60
time_label.text = "Finish time: %02d:%02d" % [minutes, seconds]
time_label.visible = true
else:
print("LevelCompleteUI: WARNING - time_label not found!")
# Hide labels that are not needed (per user request: only show enemies, coins, and time)
if downed_label: if downed_label:
downed_label.visible = false
downed_label.text = "Times Downed: " + str(times_downed) downed_label.text = "Times Downed: " + str(times_downed)
if exp_label: if exp_label:
exp_label.visible = false
exp_label.text = "EXP Collected: " + str(int(exp_collected)) exp_label.text = "EXP Collected: " + str(int(exp_collected))
if coins_label: if coins_label:
coins_label.text = "Coins Collected: " + str(coins_collected) coins_label.text = "Coins collected: " + str(coins_collected)
coins_label.visible = true
else:
print("LevelCompleteUI: WARNING - coins_label not found!")
# Show UI # Show UI
visible = true visible = true
@@ -68,9 +108,15 @@ func show_stats(enemies_defeated: int, times_downed: int, exp_collected: float,
if title_label: if title_label:
title_label.modulate.a = 0.0 title_label.modulate.a = 0.0
fade_in.tween_property(title_label, "modulate:a", 1.0, 0.5) fade_in.tween_property(title_label, "modulate:a", 1.0, 0.5)
if stats_header_label:
stats_header_label.modulate.a = 0.0
fade_in.tween_property(stats_header_label, "modulate:a", 1.0, 0.5)
if enemies_label: if enemies_label:
enemies_label.modulate.a = 0.0 enemies_label.modulate.a = 0.0
fade_in.tween_property(enemies_label, "modulate:a", 1.0, 0.5) fade_in.tween_property(enemies_label, "modulate:a", 1.0, 0.5)
if time_label:
time_label.modulate.a = 0.0
fade_in.tween_property(time_label, "modulate:a", 1.0, 0.5)
if downed_label: if downed_label:
downed_label.modulate.a = 0.0 downed_label.modulate.a = 0.0
fade_in.tween_property(downed_label, "modulate:a", 1.0, 0.5) fade_in.tween_property(downed_label, "modulate:a", 1.0, 0.5)

View File

@@ -38,6 +38,7 @@ var collected: bool = false
@onready var sfx_coin_collect = $SfxCoinCollect @onready var sfx_coin_collect = $SfxCoinCollect
@onready var sfx_loot_collect = $SfxLootCollect @onready var sfx_loot_collect = $SfxLootCollect
@onready var sfx_potion_collect = $SfxPotionCollect @onready var sfx_potion_collect = $SfxPotionCollect
@onready var sfx_banana_collect = $SfxBananaCollect
@onready var sfx_key_collect = $SfxKeyCollect @onready var sfx_key_collect = $SfxKeyCollect
func _ready(): func _ready():
@@ -102,21 +103,21 @@ func _setup_sprite():
sprite.texture = items_texture sprite.texture = items_texture
sprite.hframes = 20 sprite.hframes = 20
sprite.vframes = 14 sprite.vframes = 14
sprite.frame = (8 * 20) + 11 sprite.frame = (8 * 20) + 10
LootType.BANANA: LootType.BANANA:
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
if items_texture: if items_texture:
sprite.texture = items_texture sprite.texture = items_texture
sprite.hframes = 20 sprite.hframes = 20
sprite.vframes = 14 sprite.vframes = 14
sprite.frame = (8 * 20) + 12 sprite.frame = (8 * 20) + 11
LootType.CHERRY: LootType.CHERRY:
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
if items_texture: if items_texture:
sprite.texture = items_texture sprite.texture = items_texture
sprite.hframes = 20 sprite.hframes = 20
sprite.vframes = 14 sprite.vframes = 14
sprite.frame = (8 * 20) + 13 sprite.frame = (8 * 20) + 12
LootType.KEY: LootType.KEY:
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
if items_texture: if items_texture:
@@ -165,6 +166,14 @@ func _physics_process(delta):
# Apply gravity to Z-axis # Apply gravity to Z-axis
acceleration_z = -300.0 # Gravity acceleration_z = -300.0 # Gravity
velocity_z += acceleration_z * delta velocity_z += acceleration_z * delta
# CRITICAL: Apply damping to velocity_z to lerp it towards 0 (prevents infinite bouncing)
# Dampen more when velocity is small (closer to ground) but allow normal bounces first
var damping_factor = 8.0 # How quickly velocity_z approaches 0 (allow more visible bounces)
if abs(velocity_z) < 25.0: # More aggressive damping for very small velocities only
damping_factor = 20.0
velocity_z = lerpf(velocity_z, 0.0, 1.0 - exp(-damping_factor * delta))
position_z += velocity_z * delta position_z += velocity_z * delta
# Apply air resistance to slow down horizontal movement while airborne # Apply air resistance to slow down horizontal movement while airborne
@@ -177,24 +186,38 @@ func _physics_process(delta):
# Apply friction when on ground (dampen X/Y momentum faster) # Apply friction when on ground (dampen X/Y momentum faster)
velocity = velocity.lerp(Vector2.ZERO, 1.0 - exp(-friction * delta)) velocity = velocity.lerp(Vector2.ZERO, 1.0 - exp(-friction * delta))
# Check if we should bounce (only if not collected) # Check if we should bounce (only if not collected and velocity is significant)
# Allow bouncing but ensure it eventually stops
if not collected and abs(velocity_z) > min_bounce_velocity: if not collected and abs(velocity_z) > min_bounce_velocity:
# Bounce on floor # Bounce on floor
if loot_type == LootType.COIN and bounce_timer == 0.0: # Only play bounce sound if bounce is significant enough and timer has elapsed
# Play bounce sound for coins # CRITICAL: Only play sound if velocity is large enough and coin is actually falling (downward)
if loot_type == LootType.COIN and bounce_timer == 0.0 and abs(velocity_z) > 50.0 and velocity_z < 0.0:
# Play bounce sound for coins (only for significant downward velocities)
if sfx_coin_bounce: if sfx_coin_bounce:
# Adjust volume based on bounce velocity # Adjust volume based on bounce velocity (softer for smaller bounces)
sfx_coin_bounce.volume_db = -1.0 + (-10.0 - (velocity_z * 0.1)) var volume_multiplier = clamp(abs(velocity_z) / 100.0, 0.3, 1.0)
sfx_coin_bounce.volume_db = -3.0 + (-12.0 * (1.0 - volume_multiplier))
sfx_coin_bounce.play() sfx_coin_bounce.play()
bounce_timer = 0.08 # Prevent rapid bounce sounds bounce_timer = 0.12 # Prevent rapid bounce sounds but allow reasonable bounce rate
velocity_z = - velocity_z * bounce_restitution velocity_z = - velocity_z * bounce_restitution
# CRITICAL: Force stop bouncing if velocity is too small after bounce (prevent micro-bounces)
# Use a lower threshold to allow a few more bounces before stopping
if abs(velocity_z) < min_bounce_velocity * 0.5:
velocity_z = 0.0
is_airborne = false
else:
is_airborne = true # Still bouncing is_airborne = true # Still bouncing
else: else:
# Velocity too small or collected - stop bouncing
velocity_z = 0.0 velocity_z = 0.0
is_airborne = false is_airborne = false
else: else:
is_airborne = false is_airborne = false
# Ensure velocity_z is zero when on ground
velocity_z = 0.0
# Apply friction even when not airborne (on ground) # Apply friction even when not airborne (on ground)
velocity = velocity.lerp(Vector2.ZERO, 1.0 - exp(-friction * delta)) velocity = velocity.lerp(Vector2.ZERO, 1.0 - exp(-friction * delta))
@@ -340,7 +363,7 @@ func _process_pickup_on_server(player: Node):
player.add_coins(coin_value) player.add_coins(coin_value)
# Show floating text with item graphic and text # Show floating text with item graphic and text
var coin_texture = load("res://assets/gfx/pickups/gold_coin.png") var coin_texture = load("res://assets/gfx/pickups/gold_coin.png")
_show_floating_text(player, "+" + str(coin_value) + " coin", Color(1.0, 0.84, 0.0), 0.5, 0.5, coin_texture, 6, 1, 0) _show_floating_text(player, "+" + str(coin_value) + " COIN", Color(1.0, 0.84, 0.0), 0.5, 0.5, coin_texture, 6, 1, 0)
self.visible = false self.visible = false
@@ -358,7 +381,7 @@ func _process_pickup_on_server(player: Node):
player.heal(heal_amount) player.heal(heal_amount)
# Show floating text with item graphic and heal amount # Show floating text with item graphic and heal amount
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
_show_floating_text(player, "+" + str(int(actual_heal)) + " hp", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 11) _show_floating_text(player, "+" + str(int(actual_heal)) + " HP", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 11)
self.visible = false self.visible = false
@@ -367,8 +390,8 @@ func _process_pickup_on_server(player: Node):
await sfx_potion_collect.finished await sfx_potion_collect.finished
queue_free() queue_free()
LootType.BANANA: LootType.BANANA:
if sfx_potion_collect: if sfx_banana_collect:
sfx_potion_collect.play() sfx_banana_collect.play()
# Heal player # Heal player
var actual_heal = 0.0 var actual_heal = 0.0
if player.has_method("heal"): if player.has_method("heal"):
@@ -376,17 +399,17 @@ func _process_pickup_on_server(player: Node):
player.heal(heal_amount) player.heal(heal_amount)
# Show floating text with item graphic and heal amount # Show floating text with item graphic and heal amount
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
_show_floating_text(player, "+" + str(int(actual_heal)) + " hp", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 12) _show_floating_text(player, "+" + str(int(actual_heal)) + " HP", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 12)
self.visible = false self.visible = false
# Wait for sound to finish before removing # Wait for sound to finish before removing
if sfx_potion_collect and sfx_potion_collect.playing: if sfx_banana_collect and sfx_banana_collect.playing:
await sfx_potion_collect.finished await sfx_banana_collect.finished
queue_free() queue_free()
LootType.CHERRY: LootType.CHERRY:
if sfx_potion_collect: if sfx_banana_collect:
sfx_potion_collect.play() sfx_banana_collect.play()
# Heal player # Heal player
var actual_heal = 0.0 var actual_heal = 0.0
if player.has_method("heal"): if player.has_method("heal"):
@@ -394,13 +417,13 @@ func _process_pickup_on_server(player: Node):
player.heal(heal_amount) player.heal(heal_amount)
# Show floating text with item graphic and heal amount # Show floating text with item graphic and heal amount
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
_show_floating_text(player, "+" + str(int(actual_heal)) + " hp", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 13) _show_floating_text(player, "+" + str(int(actual_heal)) + " HP", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 13)
self.visible = false self.visible = false
# Wait for sound to finish before removing # Wait for sound to finish before removing
if sfx_potion_collect and sfx_potion_collect.playing: if sfx_banana_collect and sfx_banana_collect.playing:
await sfx_potion_collect.finished await sfx_banana_collect.finished
queue_free() queue_free()
LootType.KEY: LootType.KEY:
if sfx_key_collect: if sfx_key_collect:
@@ -410,7 +433,7 @@ func _process_pickup_on_server(player: Node):
player.add_key(1) player.add_key(1)
# Show floating text with item graphic and text # Show floating text with item graphic and text
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
_show_floating_text(player, "+1 key", Color.YELLOW, 0.5, 0.5, items_texture, 20, 14, (13 * 20) + 10) _show_floating_text(player, "+1 KEY", Color.YELLOW, 0.5, 0.5, items_texture, 20, 14, (13 * 20) + 10)
self.visible = false self.visible = false

View File

@@ -17,6 +17,7 @@ var is_local_player: bool = false
var can_send_rpcs: bool = false # Flag to prevent RPCs until player is fully initialized var can_send_rpcs: bool = false # Flag to prevent RPCs until player is fully initialized
var all_clients_ready: bool = false # Server only: true when all clients have notified they're ready var all_clients_ready: bool = false # Server only: true when all clients have notified they're ready
var all_clients_ready_time: float = 0.0 # Server only: time when all_clients_ready was set to true var all_clients_ready_time: float = 0.0 # Server only: time when all_clients_ready was set to true
var teleported_this_frame: bool = false # Flag to prevent position sync from overriding teleportation
# Input device (for local multiplayer) # Input device (for local multiplayer)
var input_device: int = -1 # -1 for keyboard, 0+ for gamepad index var input_device: int = -1 # -1 for keyboard, 0+ for gamepad index
@@ -37,6 +38,7 @@ var push_direction_locked: int = Direction.DOWN # Locked facing direction when p
var initial_grab_position = Vector2.ZERO # Position of grabbed object when first grabbed var initial_grab_position = Vector2.ZERO # Position of grabbed object when first grabbed
var initial_player_position = Vector2.ZERO # Position of player when first grabbed var initial_player_position = Vector2.ZERO # Position of player when first grabbed
var object_blocked_by_wall = false # True if pushed object is blocked by a wall var object_blocked_by_wall = false # True if pushed object is blocked by a wall
var was_dragging_last_frame = false # Track if we were dragging last frame to detect start/stop
# Being held state # Being held state
var being_held_by: Node = null var being_held_by: Node = null
@@ -720,6 +722,9 @@ func _update_z_physics(delta):
shadow.modulate.a = 0.5 shadow.modulate.a = 0.5
func _physics_process(delta): func _physics_process(delta):
# Reset teleport flag at start of frame
teleported_this_frame = false
# Update animations # Update animations
_update_animation(delta) _update_animation(delta)
@@ -963,6 +968,27 @@ func _handle_input():
if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD": if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD":
_set_animation("IDLE") _set_animation("IDLE")
# Handle drag sound for interactable objects
var is_dragging_now = false
if held_object and is_pushing and not is_lifting:
# Player is pushing (not lifting) - check if moving
if input_vector.length() > 0.1 and not object_blocked_by_wall:
# Player is moving while pushing - this is dragging
is_dragging_now = true
# Continuously play drag sound while dragging (method checks if already playing)
if held_object.has_method("play_drag_sound"):
held_object.play_drag_sound()
# Stop drag sound when stopping or not dragging
if not is_dragging_now and was_dragging_last_frame:
# Stopped dragging - stop drag sound
if held_object and held_object.has_method("stop_drag_sound"):
held_object.stop_drag_sound()
# Update drag state for next frame
was_dragging_last_frame = is_dragging_now
# Reduce speed by half when pushing/pulling # Reduce speed by half when pushing/pulling
var current_speed = move_speed * (0.5 if is_pushing else 1.0) var current_speed = move_speed * (0.5 if is_pushing else 1.0)
velocity = input_vector * current_speed velocity = input_vector * current_speed
@@ -1263,6 +1289,7 @@ func _lift_object():
_sync_grab.rpc(held_object.get_path(), true, push_axis) _sync_grab.rpc(held_object.get_path(), true, push_axis)
print("Lifted: ", held_object.name) print("Lifted: ", held_object.name)
$SfxLift.play()
func _start_pushing(): func _start_pushing():
if not held_object: if not held_object:
@@ -1321,6 +1348,11 @@ func _stop_pushing():
is_pushing = false is_pushing = false
push_axis = Vector2.ZERO push_axis = Vector2.ZERO
# Stop drag sound when releasing object
if held_object and held_object.has_method("stop_drag_sound"):
held_object.stop_drag_sound()
was_dragging_last_frame = false # Reset drag state
# Store reference and CURRENT position - don't change it! # Store reference and CURRENT position - don't change it!
var released_obj = held_object var released_obj = held_object
var released_obj_position = released_obj.global_position # Store exact position var released_obj_position = released_obj.global_position # Store exact position
@@ -1443,6 +1475,7 @@ func _throw_object():
# Play throw animation # Play throw animation
_set_animation("THROW") _set_animation("THROW")
$SfxThrow.play()
# Sync throw over network # Sync throw over network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree(): if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
@@ -1887,6 +1920,19 @@ func _sync_place_down(obj_path: NodePath, place_pos: Vector2):
if obj.has_method("set_being_held"): if obj.has_method("set_being_held"):
obj.set_being_held(false) obj.set_being_held(false)
@rpc("any_peer", "reliable")
func _sync_teleport_position(new_pos: Vector2):
# Sync teleport position from server to clients
# Server calls this to teleport any player (even if client has authority over their own player)
# Only update if we're not on the server (server already set position directly)
if not multiplayer.is_server():
global_position = new_pos
# Reset velocity to prevent player from moving back to old position
velocity = Vector2.ZERO
# Set flag to prevent position sync from overriding teleportation this frame
teleported_this_frame = true
print(name, " teleported to ", new_pos, " (synced from server, is_authority: ", is_multiplayer_authority(), ")")
@rpc("any_peer", "unreliable") @rpc("any_peer", "unreliable")
func _sync_held_object_pos(obj_path: NodePath, pos: Vector2): func _sync_held_object_pos(obj_path: NodePath, pos: Vector2):
# Sync held object position to other clients # Sync held object position to other clients
@@ -2410,6 +2456,7 @@ func _show_damage_number(amount: float, from_position: Vector2):
get_tree().current_scene.add_child(damage_label) get_tree().current_scene.add_child(damage_label)
damage_label.global_position = global_position + Vector2(0, -16) damage_label.global_position = global_position + Vector2(0, -16)
@rpc("any_peer", "reliable")
func _sync_damage(_amount: float, attacker_position: Vector2): func _sync_damage(_amount: float, attacker_position: Vector2):
# This RPC only syncs visual effects, not damage application # This RPC only syncs visual effects, not damage application
# (damage is already applied via rpc_take_damage) # (damage is already applied via rpc_take_damage)

View File

@@ -21,13 +21,13 @@ func _ready():
body_entered.connect(_on_body_entered) body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited) body_exited.connect(_on_body_exited)
# Create debug label to show puzzle type and trigger status
_create_debug_label()
# Find doors, enemies, and switches in this room (deferred to ensure doors are fully initialized) # Find doors, enemies, and switches in this room (deferred to ensure doors are fully initialized)
# This ensures doors have their blocking_room set before we try to find them # This ensures doors have their blocking_room set before we try to find them
call_deferred("_find_room_entities") call_deferred("_find_room_entities")
# Create debug label (deferred to avoid blocking on font load during startup)
call_deferred("_create_debug_label")
func _on_body_entered(body): func _on_body_entered(body):
# Player entered the room # Player entered the room
if body.is_in_group("player"): if body.is_in_group("player"):
@@ -209,7 +209,7 @@ func _find_room_entities():
print("RoomTrigger: Found ", doors_in_room.size(), " doors for room (", room.x, ", ", room.y, ")") print("RoomTrigger: Found ", doors_in_room.size(), " doors for room (", room.x, ", ", room.y, ")")
# Find enemies # Find enemies (only if room has enemies - skip for empty rooms to avoid unnecessary work)
for child in entities_node.get_children(): for child in entities_node.get_children():
if child.is_in_group("enemy"): if child.is_in_group("enemy"):
# Check if enemy is in this room # Check if enemy is in this room
@@ -231,9 +231,10 @@ func _find_room_entities():
enemy_tile_y >= room_min_y and enemy_tile_y < room_max_y: enemy_tile_y >= room_min_y and enemy_tile_y < room_max_y:
enemies_in_room.append(child) enemies_in_room.append(child)
# Find floor switches # Find floor switches (search in Entities node, not all nodes in scene)
for child in get_tree().get_nodes_in_group("floor_switch"): # This is more efficient and avoids searching the entire scene tree
if is_instance_valid(child): for child in entities_node.get_children():
if child.is_in_group("floor_switch") and is_instance_valid(child):
# Check if switch is in this room # Check if switch is in this room
var tile_size = 16 var tile_size = 16
var switch_tile_x = int(child.global_position.x / tile_size) var switch_tile_x = int(child.global_position.x / tile_size)
@@ -247,7 +248,10 @@ func _find_room_entities():
switch_tile_y >= room_min_y and switch_tile_y < room_max_y: switch_tile_y >= room_min_y and switch_tile_y < room_max_y:
floor_switches_in_room.append(child) floor_switches_in_room.append(child)
# Update debug label after finding all entities print("RoomTrigger: Found ", enemies_in_room.size(), " enemies and ", floor_switches_in_room.size(), " switches for room (", room.x, ", ", room.y, ")")
# Update debug label after finding all entities (skip if label not created yet)
if debug_label:
call_deferred("_update_debug_label") call_deferred("_update_debug_label")
func _spawn_room_enemies(): func _spawn_room_enemies():
@@ -422,6 +426,10 @@ func _find_room_spawners():
func _create_debug_label(): func _create_debug_label():
# Create a debug label to show puzzle type and trigger status above the room # Create a debug label to show puzzle type and trigger status above the room
# Skip if label already exists
if debug_label and is_instance_valid(debug_label):
return
var label = Label.new() var label = Label.new()
label.name = "DebugLabel" label.name = "DebugLabel"
@@ -440,17 +448,16 @@ func _create_debug_label():
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
# Load standard_font.png as bitmap font (it's imported as FontFile resource) # Load standard_font.png as bitmap font (it's imported as FontFile resource)
# The PNG is automatically imported as a FontFile when using font_data_image importer # Use try-catch-like approach - if font loading fails, just skip it
# Use ResourceLoader.load() to ensure we get the FontFile resource # This prevents blocking the game if font resource has issues
var standard_font_resource = ResourceLoader.load("res://assets/fonts/standard_font.png", "FontFile") var standard_font_resource = null
if standard_font_resource and standard_font_resource is FontFile: if ResourceLoader.exists("res://assets/fonts/standard_font.png"):
standard_font_resource = load("res://assets/fonts/standard_font.png")
if standard_font_resource:
label.add_theme_font_override("font", standard_font_resource) label.add_theme_font_override("font", standard_font_resource)
# Always set font size
label.add_theme_font_size_override("font_size", 8) label.add_theme_font_size_override("font_size", 8)
else:
# Fallback: just set smaller font size if font can't be loaded
label.add_theme_font_size_override("font_size", 8)
if not standard_font_resource:
print("RoomTrigger: WARNING - Could not load standard_font.png font resource!")
# Style the label # Style the label
label.add_theme_color_override("font_color", Color.YELLOW) label.add_theme_color_override("font_color", Color.YELLOW)

View File

@@ -2,6 +2,8 @@ extends Area2D
# Stairs that trigger level completion when player enters # Stairs that trigger level completion when player enters
@onready var sfx_stairs = null
func _ready(): func _ready():
# Connect body entered signal # Connect body entered signal
body_entered.connect(_on_body_entered) body_entered.connect(_on_body_entered)
@@ -10,10 +12,29 @@ func _ready():
collision_layer = 0 collision_layer = 0
collision_mask = 1 # Detect players (layer 1) collision_mask = 1 # Detect players (layer 1)
# Load stairs sound effect
if not sfx_stairs:
# Try to create AudioStreamPlayer2D if it doesn't exist
sfx_stairs = AudioStreamPlayer2D.new()
sfx_stairs.name = "SfxStairs"
add_child(sfx_stairs)
# Load go_down_stairs.mp3 sound
var stairs_sound = load("res://assets/audio/sfx/walk/go_down_stairs.mp3")
if stairs_sound:
sfx_stairs.stream = stairs_sound
else:
print("Stairs: Warning - Could not load go_down_stairs.mp3")
func _on_body_entered(body: Node2D): func _on_body_entered(body: Node2D):
print("Stairs: Body entered: ", body, " is_player: ", body.is_in_group("player") if body else false, " is_dead: ", body.is_dead if body and "is_dead" in body else false) print("Stairs: Body entered: ", body, " is_player: ", body.is_in_group("player") if body else false, " is_dead: ", body.is_dead if body and "is_dead" in body else false)
if body and body.is_in_group("player") and not body.is_dead: if body and body.is_in_group("player") and not body.is_dead:
print("Stairs: Player entered stairs! Player: ", body.name) print("Stairs: Player entered stairs! Player: ", body.name)
# Play stairs sound effect
if sfx_stairs and sfx_stairs.stream:
sfx_stairs.play()
# Only trigger on server/authority # Only trigger on server/authority
if multiplayer.is_server() or not multiplayer.has_multiplayer_peer(): if multiplayer.is_server() or not multiplayer.has_multiplayer_peer():
print("Stairs: Server detected, calling game_world") print("Stairs: Server detected, calling game_world")
@@ -25,4 +46,3 @@ func _on_body_entered(body: Node2D):
print("Stairs: ERROR - Game world not found!") print("Stairs: ERROR - Game world not found!")
else: else:
print("Stairs: Not server, ignoring") print("Stairs: Not server, ignoring")

View File

@@ -14,7 +14,7 @@ var travel_direction: Vector2 = Vector2.RIGHT
var elapsed_time: float = 0.0 var elapsed_time: float = 0.0
var distance_traveled: float = 0.0 var distance_traveled: float = 0.0
var player_owner: Node = null var player_owner: Node = null
var hit_targets = [] # Track what we've already hit var hit_targets = {} # Track what we've already hit (Dictionary for O(1) lookup)
@onready var sprite = $Sprite2D @onready var sprite = $Sprite2D
@onready var hit_area = $Area2D @onready var hit_area = $Area2D
@@ -66,7 +66,7 @@ func _on_body_entered(body):
if body == player_owner: if body == player_owner:
return return
# Don't hit the same target twice # Don't hit the same target twice - use Dictionary for O(1) lookup to prevent race conditions
if body in hit_targets: if body in hit_targets:
return return
@@ -77,7 +77,8 @@ func _on_body_entered(body):
if player_owner and not player_owner.is_multiplayer_authority(): if player_owner and not player_owner.is_multiplayer_authority():
return # Only server players can damage other players return # Only server players can damage other players
hit_targets.append(body) # Add to hit_targets IMMEDIATELY to prevent multiple hits (mark as hit before processing)
hit_targets[body] = true
# Deal damage to players - call RPC to let victim apply damage on their client # Deal damage to players - call RPC to let victim apply damage on their client
# Pass the attacker's position (not projectile position) for accurate direction # Pass the attacker's position (not projectile position) for accurate direction
@@ -125,7 +126,7 @@ func _on_body_entered(body):
# Deal damage to boxes or other damageable objects # Deal damage to boxes or other damageable objects
elif "health" in body: elif "health" in body:
$SfxImpact.play() #$SfxImpact.play()
# Boxes have health property # Boxes have health property
body.health -= damage body.health -= damage
if body.health <= 0: if body.health <= 0:

View File

@@ -12,7 +12,7 @@ var swing_start_angle: float = 0.0 # Starting angle
var swing_arc: float = 180.0 # Total arc to swing (180 degrees) var swing_arc: float = 180.0 # Total arc to swing (180 degrees)
var elapsed_time: float = 0.0 var elapsed_time: float = 0.0
var player_owner: Node = null var player_owner: Node = null
var hit_targets = [] # Track what we've already hit var hit_targets = {} # Track what we've already hit (Dictionary for O(1) lookup)
@onready var sprite = $Sprite2D @onready var sprite = $Sprite2D
@onready var hit_area = $Area2D @onready var hit_area = $Area2D
@@ -57,11 +57,12 @@ func _on_body_entered(body):
if body == player_owner: if body == player_owner:
return return
# Don't hit the same target twice # Don't hit the same target twice - use Dictionary for O(1) lookup to prevent race conditions
if body in hit_targets: if body in hit_targets:
return return
hit_targets.append(body) # Add to hit_targets IMMEDIATELY to prevent multiple hits (mark as hit before processing)
hit_targets[body] = true
# Deal damage to players # Deal damage to players
if body.is_in_group("player") and body.has_method("take_damage"): if body.is_in_group("player") and body.has_method("take_damage"):

View File

@@ -8,26 +8,28 @@ func _ready() -> void:
# Called every frame. 'delta' is the elapsed time since the previous frame. # Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void: func _process(_delta: float) -> void:
pass pass
func _on_area_which_teleports_player_into_room_body_entered(body: Node2D) -> void: func _on_area_which_teleports_player_into_room_body_entered(body: Node2D) -> void:
if !is_enabled: if !is_enabled:
return return
# TODO: teleport player passed the door... # TODO: add some kind of animation
if (!$TeleportSfx.playing):
$TeleportSfx.play()
get_parent().teleportPlayer(body) get_parent().teleportPlayer(body)
pass # Replace with function body. pass # Replace with function body.
func _on_area_to_start_emit_body_entered(body: Node2D) -> void: func _on_area_to_start_emit_body_entered(_body: Node2D) -> void:
if !is_enabled: if !is_enabled:
return return
$GPUParticles2D.emitting = true $GPUParticles2D.emitting = true
pass # Replace with function body. pass # Replace with function body.
func _on_area_to_start_emit_body_exited(body: Node2D) -> void: func _on_area_to_start_emit_body_exited(_body: Node2D) -> void:
if !is_enabled: if !is_enabled:
return return
$GPUParticles2D.emitting = false $GPUParticles2D.emitting = false