diff --git a/src/assets/audio/sfx/nickes/gate/gate_down_01.wav.mp3 b/src/assets/audio/sfx/nickes/gate/gate_down_01.wav.mp3 deleted file mode 100644 index 3f0b799..0000000 Binary files a/src/assets/audio/sfx/nickes/gate/gate_down_01.wav.mp3 and /dev/null differ diff --git a/src/assets/audio/sfx/nickes/gate/gate_down_01.wav.mp3.import b/src/assets/audio/sfx/nickes/gate/gate_down_01.wav.mp3.import deleted file mode 100644 index 56ea9fa..0000000 --- a/src/assets/audio/sfx/nickes/gate/gate_down_01.wav.mp3.import +++ /dev/null @@ -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 diff --git a/src/assets/audio/sfx/nickes/gate/gate_up_01.wav.mp3 b/src/assets/audio/sfx/nickes/gate/gate_up_01.wav.mp3 deleted file mode 100644 index ad132f9..0000000 Binary files a/src/assets/audio/sfx/nickes/gate/gate_up_01.wav.mp3 and /dev/null differ diff --git a/src/assets/audio/sfx/nickes/gate/gate_up_01.wav.mp3.import b/src/assets/audio/sfx/nickes/gate/gate_up_01.wav.mp3.import deleted file mode 100644 index dfc3e33..0000000 --- a/src/assets/audio/sfx/nickes/gate/gate_up_01.wav.mp3.import +++ /dev/null @@ -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 diff --git a/src/assets/audio/sfx/nickes/gate/sword-impact-metalgate-01.wav.mp3 b/src/assets/audio/sfx/nickes/gate/sword-impact-metalgate-01.wav.mp3 deleted file mode 100644 index da25fb0..0000000 Binary files a/src/assets/audio/sfx/nickes/gate/sword-impact-metalgate-01.wav.mp3 and /dev/null differ diff --git a/src/assets/audio/sfx/nickes/gate/sword-impact-metalgate-01.wav.mp3.import b/src/assets/audio/sfx/nickes/gate/sword-impact-metalgate-01.wav.mp3.import deleted file mode 100644 index d6dffee..0000000 --- a/src/assets/audio/sfx/nickes/gate/sword-impact-metalgate-01.wav.mp3.import +++ /dev/null @@ -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 diff --git a/src/assets/audio/sfx/nickes/gate/sword-impact-metalgate-02.wav.mp3 b/src/assets/audio/sfx/nickes/gate/sword-impact-metalgate-02.wav.mp3 deleted file mode 100644 index 8ebcee3..0000000 Binary files a/src/assets/audio/sfx/nickes/gate/sword-impact-metalgate-02.wav.mp3 and /dev/null differ diff --git a/src/assets/audio/sfx/nickes/gate/sword-impact-metalgate-02.wav.mp3.import b/src/assets/audio/sfx/nickes/gate/sword-impact-metalgate-02.wav.mp3.import deleted file mode 100644 index ab5b54a..0000000 --- a/src/assets/audio/sfx/nickes/gate/sword-impact-metalgate-02.wav.mp3.import +++ /dev/null @@ -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 diff --git a/src/assets/audio/sfx/nickes/move_rock/move_rock.mp3.wav.mp3 b/src/assets/audio/sfx/nickes/move_rock/move_rock.mp3.wav.mp3 deleted file mode 100644 index 6a38c25..0000000 Binary files a/src/assets/audio/sfx/nickes/move_rock/move_rock.mp3.wav.mp3 and /dev/null differ diff --git a/src/assets/audio/sfx/nickes/move_rock/move_rock.mp3.wav.mp3.import b/src/assets/audio/sfx/nickes/move_rock/move_rock.mp3.wav.mp3.import deleted file mode 100644 index 2461b54..0000000 --- a/src/assets/audio/sfx/nickes/move_rock/move_rock.mp3.wav.mp3.import +++ /dev/null @@ -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 diff --git a/src/assets/audio/sfx/nickes/move_rock/rock_push_loop_01.mp3 b/src/assets/audio/sfx/nickes/move_rock/rock_push_loop_01.mp3 deleted file mode 100644 index 592549c..0000000 Binary files a/src/assets/audio/sfx/nickes/move_rock/rock_push_loop_01.mp3 and /dev/null differ diff --git a/src/assets/audio/sfx/nickes/move_rock/rock_push_loop_01.mp3.import b/src/assets/audio/sfx/nickes/move_rock/rock_push_loop_01.mp3.import deleted file mode 100644 index a2bda3e..0000000 --- a/src/assets/audio/sfx/nickes/move_rock/rock_push_loop_01.mp3.import +++ /dev/null @@ -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 diff --git a/src/assets/audio/sfx/nickes/move_rock/rock_push_loop_02.mp3 b/src/assets/audio/sfx/nickes/move_rock/rock_push_loop_02.mp3 deleted file mode 100644 index 6f23997..0000000 Binary files a/src/assets/audio/sfx/nickes/move_rock/rock_push_loop_02.mp3 and /dev/null differ diff --git a/src/assets/audio/sfx/nickes/move_rock/rock_push_loop_02.mp3.import b/src/assets/audio/sfx/nickes/move_rock/rock_push_loop_02.mp3.import deleted file mode 100644 index ae031a8..0000000 --- a/src/assets/audio/sfx/nickes/move_rock/rock_push_loop_02.mp3.import +++ /dev/null @@ -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 diff --git a/src/assets/audio/sfx/nickes/treasure/coin_drop_01.wav.mp3 b/src/assets/audio/sfx/nickes/treasure/coin_drop_01.wav.mp3 deleted file mode 100644 index 0a07957..0000000 Binary files a/src/assets/audio/sfx/nickes/treasure/coin_drop_01.wav.mp3 and /dev/null differ diff --git a/src/assets/audio/sfx/nickes/treasure/coin_drop_01.wav.mp3.import b/src/assets/audio/sfx/nickes/treasure/coin_drop_01.wav.mp3.import deleted file mode 100644 index e22662c..0000000 --- a/src/assets/audio/sfx/nickes/treasure/coin_drop_01.wav.mp3.import +++ /dev/null @@ -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 diff --git a/src/assets/audio/sfx/nickes/treasure/coin_drop_02.wav.mp3 b/src/assets/audio/sfx/nickes/treasure/coin_drop_02.wav.mp3 deleted file mode 100644 index fc09f2c..0000000 Binary files a/src/assets/audio/sfx/nickes/treasure/coin_drop_02.wav.mp3 and /dev/null differ diff --git a/src/assets/audio/sfx/nickes/treasure/coin_drop_02.wav.mp3.import b/src/assets/audio/sfx/nickes/treasure/coin_drop_02.wav.mp3.import deleted file mode 100644 index 8e42d6b..0000000 --- a/src/assets/audio/sfx/nickes/treasure/coin_drop_02.wav.mp3.import +++ /dev/null @@ -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 diff --git a/src/assets/audio/sfx/nickes/treasure/open_chest_01.wav.mp3 b/src/assets/audio/sfx/nickes/treasure/open_chest_01.wav.mp3 deleted file mode 100644 index ab4dfb9..0000000 Binary files a/src/assets/audio/sfx/nickes/treasure/open_chest_01.wav.mp3 and /dev/null differ diff --git a/src/assets/audio/sfx/nickes/treasure/open_chest_01.wav.mp3.import b/src/assets/audio/sfx/nickes/treasure/open_chest_01.wav.mp3.import deleted file mode 100644 index ceed371..0000000 --- a/src/assets/audio/sfx/nickes/treasure/open_chest_01.wav.mp3.import +++ /dev/null @@ -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 diff --git a/src/assets/audio/sfx/nickes/treasure/open_chest_02.wav.mp3 b/src/assets/audio/sfx/nickes/treasure/open_chest_02.wav.mp3 deleted file mode 100644 index 485c81c..0000000 Binary files a/src/assets/audio/sfx/nickes/treasure/open_chest_02.wav.mp3 and /dev/null differ diff --git a/src/assets/audio/sfx/nickes/treasure/open_chest_02.wav.mp3.import b/src/assets/audio/sfx/nickes/treasure/open_chest_02.wav.mp3.import deleted file mode 100644 index cd1d31b..0000000 --- a/src/assets/audio/sfx/nickes/treasure/open_chest_02.wav.mp3.import +++ /dev/null @@ -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 diff --git a/src/assets/audio/sfx/nickes/treasure/open_treasure_01.wav.mp3 b/src/assets/audio/sfx/nickes/treasure/open_treasure_01.wav.mp3 deleted file mode 100644 index 4fba7c0..0000000 Binary files a/src/assets/audio/sfx/nickes/treasure/open_treasure_01.wav.mp3 and /dev/null differ diff --git a/src/assets/audio/sfx/nickes/treasure/open_treasure_01.wav.mp3.import b/src/assets/audio/sfx/nickes/treasure/open_treasure_01.wav.mp3.import deleted file mode 100644 index 7deaf95..0000000 --- a/src/assets/audio/sfx/nickes/treasure/open_treasure_01.wav.mp3.import +++ /dev/null @@ -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 diff --git a/src/scenes/TeleporterIntoClosedRoom.tscn b/src/scenes/TeleporterIntoClosedRoom.tscn index 67923c7..8ae98e1 100644 --- a/src/scenes/TeleporterIntoClosedRoom.tscn +++ b/src/scenes/TeleporterIntoClosedRoom.tscn @@ -1,6 +1,7 @@ [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="AudioStream" uid="uid://digavvapakqaw" path="res://assets/audio/sfx/teleport.ogg" id="2_7twcj"] [sub_resource type="Gradient" id="Gradient_skeae"] offsets = PackedFloat32Array(0, 0.9883721) @@ -97,18 +98,23 @@ randomness = 1.0 process_material = SubResource("ParticleProcessMaterial_n1yim") [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] shape = SubResource("RectangleShape2D_pp12y") [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] shape = SubResource("RectangleShape2D_7twcj") 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="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"] diff --git a/src/scenes/door.tscn b/src/scenes/door.tscn index d07e9cd..5e30d03 100644 --- a/src/scenes/door.tscn +++ b/src/scenes/door.tscn @@ -5,13 +5,22 @@ [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://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"] -size = Vector2(26, 14) +size = Vector2(26, 16) [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] collision_layer = 64 @@ -21,27 +30,45 @@ script = ExtResource("1_uvdjg") position = Vector2(0, -16) [node name="Sprite2D" type="Sprite2D" parent="." unique_id=1520856168] +position = Vector2(0, -1) texture = ExtResource("1_hpvv5") [node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=1691515105] shape = SubResource("RectangleShape2D_uvdjg") +metadata/_edit_lock_ = true [node name="SfxOpenKeyDoor" type="AudioStreamPlayer2D" parent="." unique_id=47303726] stream = ExtResource("3_la1wf") +max_distance = 1485.0 +attenuation = 6.276666 +panning_strength = 1.09 [node name="SfxOpenStoneDoor" type="AudioStreamPlayer2D" parent="." unique_id=885417421] stream = ExtResource("4_18pbm") +max_distance = 1204.0 +attenuation = 6.498014 +panning_strength = 1.25 [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] +metadata/_edit_lock_ = true [node name="CollisionShape2D" type="CollisionShape2D" parent="KeyInteractionArea" unique_id=1640987231] shape = SubResource("RectangleShape2D_la1wf") debug_color = Color(0.70196074, 0.67558956, 0.17869899, 0.41960785) [node name="SfxDoorCloses" type="AudioStreamPlayer2D" parent="." unique_id=1074871158] -stream = ExtResource("5_18pbm") -max_distance = 1333.0 -attenuation = 5.8563395 +stream = SubResource("AudioStreamRandomizer_ey00f") +volume_db = -1.268 +max_distance = 1289.0 +attenuation = 3.7321312 + +[node name="SfxCloseGateDoor" type="AudioStreamPlayer2D" parent="." unique_id=1825261269] +stream = ExtResource("8_pg2b6") diff --git a/src/scenes/interactable_object.tscn b/src/scenes/interactable_object.tscn index 5fe28ac..6341e7c 100644 --- a/src/scenes/interactable_object.tscn +++ b/src/scenes/interactable_object.tscn @@ -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://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://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"] radius = 4.0 @@ -54,6 +61,25 @@ stream_0/stream = ExtResource("4_gpwir") stream_1/stream = ExtResource("5_nb01e") 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] collision_layer = 2 collision_mask = 71 @@ -110,3 +136,14 @@ max_distance = 750.0 attenuation = 10.556063 panning_strength = 1.5 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 diff --git a/src/scenes/loot.tscn b/src/scenes/loot.tscn index d84d1f5..d8d8a16 100644 --- a/src/scenes/loot.tscn +++ b/src/scenes/loot.tscn @@ -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://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://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"] colors = PackedColorArray(0, 0, 0, 1, 0, 0, 0, 0) @@ -25,6 +27,13 @@ radius = 3.0 [sub_resource type="CircleShape2D" id="CircleShape2D_1"] 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] collision_layer = 0 collision_mask = 64 @@ -73,3 +82,6 @@ stream = ExtResource("6_gyjv8") [node name="SfxPotionCollect" type="AudioStreamPlayer2D" parent="." unique_id=1615824668] stream = ExtResource("7_eeo7l") + +[node name="SfxBananaCollect" type="AudioStreamPlayer2D" parent="." unique_id=1763488179] +stream = SubResource("AudioStreamRandomizer_37k03") diff --git a/src/scenes/player.tscn b/src/scenes/player.tscn index 0239187..f48e1aa 100644 --- a/src/scenes/player.tscn +++ b/src/scenes/player.tscn @@ -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://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://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"] offsets = PackedFloat32Array(0.7710843, 0.77710843) @@ -196,3 +198,16 @@ bus = &"Sfx" stream = SubResource("AudioStreamRandomizer_487ah") attenuation = 7.7274756 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 diff --git a/src/scripts/door.gd b/src/scripts/door.gd index e79f533..6bcf6ec 100644 --- a/src/scripts/door.gd +++ b/src/scripts/door.gd @@ -9,7 +9,7 @@ extends StaticBody2D @export var is_closed: bool = true var is_closing: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 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_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. func _ready() -> void: # 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 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") + + # Spawn smoke puffs when StoneDoor finishes opening (1-2 puffs) + if type == "StoneDoor": + _spawn_smoke_puffs_on_open() + # Animation finished, reset flags is_opening = false is_closing = false @@ -222,6 +230,11 @@ func _process(delta: float) -> void: # When at closed position, collision should be ENABLED set_collision_layer_value(7, true) 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 is_opening = false is_closing = false @@ -295,6 +308,10 @@ func _update_collision_based_on_position(): set_collision_layer_value(7, false) 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 # CRITICAL: For KeyDoors, ensure they start from closed position before opening # KeyDoors should ALWAYS start from closed position when opening (never from open position) @@ -338,8 +355,10 @@ func _open(): else: push_error("Door: StoneDoor/GateDoor _open() called but closed_position is zero!") return - - $SfxOpenStoneDoor.play() + if type == "GateDoor": + $SfxOpenGateDoor.play() + else: + $SfxOpenStoneDoor.play() # CRITICAL: Store starting position for animation (should be closed_position) animation_start_position = position @@ -348,7 +367,17 @@ func _open(): is_closing = false 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(): + # 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) if type == "KeyDoor": print("Door: ERROR - _close() called on KeyDoor! KeyDoors should never be closed!") @@ -401,11 +430,18 @@ func _close(): animation_start_position = position print("Door: Starting close animation from ", animation_start_position, " to ", closed_position, " (offset: ", open_offset, ")") - $SfxDoorCloses.play() + if type == "GateDoor": + $SfxCloseGateDoor.play() + else: + $SfxDoorCloses.play() is_opening = false is_closing = true move_timer = 0.0 $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(): # Called after door is fully set up with room references and positioned @@ -646,27 +682,77 @@ func _on_room_exited(body): # Doors stay in their current state func _check_puzzle_state(): - # Check if room puzzle is solved - # IMPORTANT: Only check puzzle state if we're in the blocking room - if puzzle_solved: - return # Already solved + # Only check puzzle state on server/authority in multiplayer + if multiplayer.has_multiplayer_peer() and not multiplayer.is_server(): + return # Clients wait for server to check and sync via RPC + + # 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) 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 "?", ")") enemies_defeated = true puzzle_solved = true - if is_closed: + if is_actually_closed: _open() return # 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 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() 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: # 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) var enemy_in_room = false - var tile_size = 16 - var enemy_tile_x = int(child.global_position.x / tile_size) - var enemy_tile_y = int(child.global_position.y / tile_size) - 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 + # Use tile_size and room bounds from parent scope (declared below) + var enemy_tile_x = int(child.global_position.x / 16) + var enemy_tile_y = int(child.global_position.y / 16) + var enemy_room_min_x = target_room.x + 2 + var enemy_room_max_x = target_room.x + target_room.w - 2 + var enemy_room_min_y = target_room.y + 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 \ - enemy_tile_y >= room_min_y and enemy_tile_y < room_max_y: + if enemy_tile_x >= enemy_room_min_x and enemy_tile_x < enemy_room_max_x and \ + enemy_tile_y >= enemy_room_min_y and enemy_tile_y < enemy_room_max_y: enemy_in_room = true # Also check spawner metadata - if enemy has spawner_name matching this room's spawners if child.has_meta("spawner_name"): @@ -718,10 +804,247 @@ func _are_all_enemies_defeated() -> bool: # 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 "?", ")") + # 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: - # No spawned enemies found - if door requires enemies, puzzle is not solved - # But if there were never any enemies, this might mean they haven't spawned yet or all are already dead/removed - print("Door: No spawned enemies found in room - puzzle not solved yet (enemies may not have spawned or already removed)") + # 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) + + 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)") return false for enemy in room_spawned_enemies: @@ -743,6 +1066,64 @@ func _are_all_enemies_defeated() -> bool: print("Door: All ", room_spawned_enemies.size(), " spawned enemies are dead! Puzzle solved!") 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: # Check if all required switches are activated # CRITICAL: ONLY check connected_switches - switches are explicitly connected when spawned @@ -795,6 +1176,11 @@ func _show_key_indicator(): key_indicator.visible = true 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 match direction: "Up": @@ -805,5 +1191,93 @@ func teleportPlayer(body: Node2D): keydoor_open_offset = Vector2(16, 0) # Open is 16px LEFT from closed "Right": 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 + +@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") diff --git a/src/scripts/dungeon_generator.gd b/src/scripts/dungeon_generator.gd index 98aae91..5df64d7 100644 --- a/src/scripts/dungeon_generator.gd +++ b/src/scripts/dungeon_generator.gd @@ -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) # 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) 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 "blocking_room" in door_data and not door_data.blocking_room.is_empty(): 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] # Skip start room and exit room 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) # 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) # Use door tile coordinates (10,2) + offset for 2x3 door 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) 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) # Use door tile coordinates (5,2) + offset for 2x3 door 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) # 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) # Use door tile coordinates (7,5) + offset for 3x2 door 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) 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) # Use door tile coordinates (7,0) + offset for 3x2 door 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) # room2 = room the door leads TO (bottom room for vertical doors) @@ -744,35 +764,122 @@ 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 # 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): - # Check if this is a valid left wall position (use room.x + 1, the right part of the left wall) - if _is_valid_torch_position(room.x + 1, y, grid, all_doors): - # Place at the same distance from floor as top wall torches + # Check if this is a valid left wall position + # Left wall has 2 tiles: room.x and room.x + 1 + # 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 # 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 # 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 world_pos = Vector2(left_wall_x, left_wall_y) - valid_wall_positions.append({"pos": world_pos, "wall": "left", "rotation": 270}) - break # Only add one torch per wall + + # 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}) + break # Only add one torch per wall # Right wall (2 tiles wide: room.x + room.w - 2 and room.x + room.w - 1) # Place torches at the same distance from floor as top wall torches # 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) # 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): - # Check if this is a valid right wall position (use room.x + room.w - 2, the left part of the right wall) - if _is_valid_torch_position(room.x + room.w - 2, y, grid, all_doors): - # Place at the same distance from floor as top wall torches + # Check if this is a valid right wall position + # Right wall has 2 tiles: room.x + room.w - 2 (left part) and room.x + room.w - 1 (right part/corner) + # 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 # 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 # 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 world_pos = Vector2(right_wall_x, right_wall_y) - valid_wall_positions.append({"pos": world_pos, "wall": "right", "rotation": 90}) - break # Only add one torch per wall + + # 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}) + break # Only add one torch per wall # Randomly select torch positions if valid_wall_positions.size() == 0: @@ -813,13 +920,25 @@ func _is_valid_torch_position(x: int, y: int, grid: Array, all_doors: Array) -> for door in all_doors: var door_x = door.x var door_y = door.y - var door_w = door.w if "w" in door else 2 # Default door width (2 or 3) - var door_h = door.h if "h" in door else 3 # Default door height (2 or 3) + var door_dir = door.dir if "dir" in door else "" + + # 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 - # For horizontal doors: door.w is width (2 or 3), door.h is 1 - # 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: + if x >= door_x and x < door_x + actual_w and y >= door_y and y < door_y + actual_h: return false return true @@ -1161,14 +1280,34 @@ func _place_enemies_in_room(room: Dictionary, grid: Array, map_size: Vector2i, r else: move_speed = rng.randf_range(50.0, 80.0) # Other enemies (humanoids): faster - enemies.append({ + var enemy_data = { "type": enemy_type, "position": position, "room": room, # Store reference to room for AI "max_health": max_health, "move_speed": move_speed, "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 @@ -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) 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 # Small rooms (7-8 tiles): 0-1 objects # Medium rooms (9-10 tiles): 0-3 objects # Large rooms (11-12 tiles): 0-8 objects # 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 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) var floor_w = room.w - 4 # Excluding 2-tile walls on each side var floor_h = room.h - 4 var floor_area = floor_w * floor_h # 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) max_objects = 1 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) + # 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 var object_types = [ {"type": "Pot", "setup": "setup_pot"}, @@ -1737,23 +1901,57 @@ func _place_interactable_objects_in_room(room: Dictionary, grid: Array, map_size if _is_valid_interactable_position(world_pos, all_doors, all_enemies, room): 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 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 # Shuffle positions to randomize placement valid_positions.shuffle() - # Place objects - for i in range(min(num_objects, valid_positions.size())): - var object_type_data = object_types[rng.randi() % object_types.size()] - var position = valid_positions[i] - + # 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": object_type_data.type, - "setup_function": object_type_data.setup, - "position": position, + "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())): + var object_type_data = object_types[rng.randi() % object_types.size()] + var position = valid_positions[i] + + objects.append({ + "type": object_type_data.type, + "setup_function": object_type_data.setup, + "position": position, + "room": room + }) return objects @@ -1872,7 +2070,7 @@ func _find_rooms_before_door(door: Dictionary, start_room: Dictionary, _all_room 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 # Returns array of blocking door data dictionaries 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 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()): if i == start_room_index or i == exit_room_index: 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] if rng.randf() < puzzle_room_chance: + print("DungeonGenerator: Room (", room.x, ", ", room.y, ") selected for puzzle assignment") # This room has a puzzle! # CRITICAL SAFETY CHECK: Never assign puzzles to start or exit rooms # Double-check even though we skip them in the loop if i == start_room_index or i == exit_room_index: continue - # Find all doors that lead OUT OF this room (doors IN this room that exit to other rooms) - # These are doors where room1 == this room (doors that start FROM this puzzle room) - var doors_out_of_room = [] + # Find all doors that are connected to this puzzle room + var doors_in_room = [] for door in all_doors: - # CRITICAL: Find doors where room1 == this room (doors that lead OUT OF this room) - if not "room1" in door or not door.room1 or door.room1.is_empty(): - continue + 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 - var door_room1 = door.room1 - # 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 \ - door_room1.w == room.w and door_room1.h == room.h) + var room_matches = false - if door_leads_out_of_this_room: - # CRITICAL: Also check that this door doesn't lead into start or exit room - if not "room2" in door or not door.room2 or door.room2.is_empty(): - continue - - 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 - - # Skip if door leads into start or exit room - if door_room2_index == start_room_index or door_room2_index == exit_room_index: - continue - - doors_out_of_room.append(door) + # 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) + + # Check if room2 matches puzzle room + if not room_matches and door_room2: + room_matches = (door_room2.x == room.x and door_room2.y == room.y and \ + door_room2.w == room.w and door_room2.h == room.h) + + # Door is connected to puzzle room + if room_matches: + doors_in_room.append(door) - if doors_out_of_room.size() == 0: - continue # No doors leading out of this room, skip + if doors_in_room.size() == 0: + print("DungeonGenerator: Room (", room.x, ", ", room.y, ") has no doors connected - skipping puzzle assignment") + continue # No doors connected to this room, skip + + print("DungeonGenerator: Room (", room.x, ", ", room.y, ") has ", doors_in_room.size(), " doors - assigning puzzle") # Decide puzzle type: 33% walk switch, 33% pillar switch, 33% enemy spawner (if room is large enough) 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 room_puzzle_data[room] = { "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 - for door in doors_out_of_room: + for door in doors_in_room: assigned_doors.append(door) + print("DungeonGenerator: Assigned puzzles to ", room_puzzle_data.size(), " rooms") + # STEP 2: Create blocking doors for rooms with puzzles # 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! @@ -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 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: - # CRITICAL: Verify this door is in the puzzle room (already checked above, but double-check) - if not "room1" in door or not door.room1 or door.room1.is_empty(): - push_error("DungeonGenerator: ERROR - Door in puzzle room (", room.x, ", ", room.y, ") has no room1! Skipping door.") - continue + # Determine direction based on which WALL of the PUZZLE ROOM the door is on + var direction = _determine_door_direction_for_puzzle_room(door, room, all_rooms) - var door_room1 = door.room1 - # CRITICAL: Verify door.room1 matches the puzzle room EXACTLY (value comparison, not reference) - var door_in_puzzle_room = (door_room1.x == room.x and door_room1.y == room.y and \ - door_room1.w == room.w and door_room1.h == room.h) + # CRITICAL: door.x and door.y are the position in room1, not necessarily in puzzle room + # Need to calculate the correct position on the puzzle room's wall + var door_room2 = door.room2 if ("room2" in door and door.room2 and not door.room2.is_empty()) else null - if not door_in_puzzle_room: - push_error("DungeonGenerator: ERROR - Door room1 (", door_room1.x, ", ", door_room1.y, ") does NOT match puzzle room (", room.x, ", ", room.y, ")! Skipping door.") - continue # This door is not in the puzzle room, skip - DO NOT CREATE DOOR + # Determine if puzzle room is room2 (if so, door position needs adjustment) + var puzzle_is_room2 = false + 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 - # (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 + # Calculate door position on the puzzle room's wall var door_tile_x = door.x var door_tile_y = door.y var open_tile_x = door_tile_x 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: + "Up": + # Door on top wall of puzzle room - door.y should be at puzzle_room.y + open_tile_x = door_tile_x # Keep same X (horizontal position) + open_tile_y = room.y # Top wall of puzzle room + "Down": + # Door on bottom wall of puzzle room - door.y should be at puzzle_room.y + room.h - 1 + open_tile_x = door_tile_x # Keep same X (horizontal position) + open_tile_y = room.y + room.h - 1 # Bottom wall of puzzle room + "Left": + # Door on left wall of puzzle room - door.x should be at puzzle_room.x + open_tile_x = room.x # Left wall of puzzle room + 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 tile 2 (row 0, col 2) = door_x+2, door_y+0 - open_tile_x = door_tile_x + 1 # col 2 (middle column, not rightmost) - open_tile_y = door_tile_y + 0 # row 0 (top row) + # 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 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) + # 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): StoneDoor/GateDoor start OPEN at (col 1, row 1) = door_x+1, door_y+1 - # When entering room, they CLOSE to (col 1, row 0) = door_x+1, door_y+0 (16px up from open) - # When solving puzzle, they OPEN back to (col 1, row 1) = door_x+1, door_y+1 - 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) + # 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 tile 3 (col 1, row 0) = door_x+1, door_y+0 - open_tile_x = door_tile_x + 0 # col 0 (left column) - open_tile_y = door_tile_y + 1 # row 1 (middle row) + # 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) # 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" } - # CRITICAL: Store room1 and room2 from original door for verification - # Ensure room1 matches blocking_room (puzzle room) - 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 + # Store puzzle room as room1 for blocking doors + door_data.original_room1 = room # Puzzle room is always room1 for blocking doors - 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, ") - door.room1: (", door_room1.x, ",", door_room1.y, "), door.room2: ", door_room2_str, ", direction: ", direction, ", open_tile: (", open_tile_x, ",", open_tile_y, ")") + print("DungeonGenerator: Creating blocking door for puzzle room (", room.x, ", ", room.y, ") - 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) # 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") 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) - 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 + # Add door to blocking doors list ONLY if it has valid puzzle element 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) 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) - 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: # 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: # Determine door direction based on door position and connected rooms # 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: return "Up" # Default @@ -2436,10 +2591,10 @@ func _determine_door_direction(door: Dictionary, _all_rooms: Array) -> String: var room2 = door.room2 # 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 below room1, door is on bottom wall (Down) - # If room2 is left of room1, door is on left wall (Left) - # If room2 is right of room1, door is on right wall (Right) + # If room2 is above room1, door is on top wall of room1 (Up) + # If room2 is below room1, door is on bottom wall of room1 (Down) + # 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 of room1 (Right) var dx = room2.x - room1.x 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): # Vertical alignment if dy < 0: - return "Up" # room2 is above room1 + return "Up" # room2 is above room1 - door on top wall of room1 else: - return "Down" # room2 is below room1 + return "Down" # room2 is below room1 - door on bottom wall of room1 else: # Horizontal alignment if dx < 0: - return "Left" # room2 is left of room1 + return "Left" # room2 is left of room1 - door on left wall of room1 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 diff --git a/src/scripts/enemy_base.gd b/src/scripts/enemy_base.gd index d7f0ead..399380e 100644 --- a/src/scripts/enemy_base.gd +++ b/src/scripts/enemy_base.gd @@ -11,6 +11,7 @@ var current_health: float = 50.0 var is_dead: bool = false var target_player: Node = null var attack_timer: float = 0.0 +var killer_player: Node = null # Track who killed this enemy (for kill credit) # Knockback var is_knocked_back: bool = false @@ -48,7 +49,7 @@ func _ready(): # CRITICAL: Set collision mask to include interactable objects (layer 2) and walls (layer 7) # This allows enemies to collide with interactable objects so they can path around them # Walls are on layer 7 (bit 6 = 64), not layer 4! - collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64) + collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64) func _physics_process(delta): if is_dead: @@ -162,18 +163,18 @@ func _check_interactable_object_collision(): # Try to path around the object by moving perpendicular to collision normal # This creates a side-stepping behavior to go around obstacles - var perpendicular = Vector2(-collision_normal.y, collision_normal.x) # Rotate 90 degrees + var perpendicular = Vector2(-collision_normal.y, collision_normal.x) # Rotate 90 degrees # Choose perpendicular direction that moves toward target (if we have one) if target_player and is_instance_valid(target_player): var to_target = (target_player.global_position - global_position).normalized() # If perpendicular dot product with target direction is negative, flip it if perpendicular.dot(to_target) < 0: - perpendicular = -perpendicular + perpendicular = - perpendicular # Apply perpendicular movement (side-step around object) - var side_step_velocity = perpendicular * move_speed * 0.7 # 70% of move speed for side-step - velocity = velocity.lerp(side_step_velocity, 0.3) # Smoothly blend with existing velocity + var side_step_velocity = perpendicular * move_speed * 0.7 # 70% of move speed for side-step + velocity = velocity.lerp(side_step_velocity, 0.3) # Smoothly blend with existing velocity # Also add some push-away from object to create clearance var push_away = collision_normal * move_speed * 0.3 @@ -184,7 +185,7 @@ func _check_interactable_object_collision(): velocity = velocity.normalized() * move_speed # For humanoid enemies, sometimes try to destroy the object - if has_method("_try_attack_object") and randf() < 0.1: # 10% chance per frame when blocked + if has_method("_try_attack_object") and randf() < 0.1: # 10% chance per frame when blocked call("_try_attack_object", blocked_objects[0].object) func _attack_player(player): @@ -239,6 +240,25 @@ func _find_nearest_player_in_range(max_range: float) -> Node: 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): # Only process damage on server/authority if not is_multiplayer_authority(): @@ -247,6 +267,12 @@ func take_damage(amount: float, from_position: Vector2): if is_dead: 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 print(name, " took ", amount, " damage! Health: ", current_health) @@ -279,8 +305,20 @@ func take_damage(amount: float, from_position: Vector2): _sync_damage_visual.rpc() 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 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") func rpc_take_damage(amount: float, from_position: Vector2): @@ -318,7 +356,7 @@ func _show_damage_number(amount: float, from_position: Vector2): var entities_node = game_world.get_node_or_null("Entities") if entities_node: entities_node.add_child(damage_label) - damage_label.global_position = global_position + Vector2(0, -16) # Above enemy head + damage_label.global_position = global_position + Vector2(0, -16) # Above enemy head else: get_tree().current_scene.add_child(damage_label) damage_label.global_position = global_position + Vector2(0, -16) @@ -347,6 +385,27 @@ func _on_take_damage(): # Override in subclasses for custom damage reactions 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): # Virtual function - override in subclasses that use animation state system # (e.g., enemy_humanoid.gd uses player-like animation system) @@ -359,6 +418,11 @@ func _die(): is_dead = true 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() @@ -465,7 +529,7 @@ func _sync_position(pos: Vector2, vel: Vector2, z_pos: float = 0.0, dir: int = 0 # Clients receive position and animation updates from server # Only process if we're not the authority (i.e., we're a client) if is_multiplayer_authority(): - return # Server ignores its own updates + return # Server ignores its own updates # Debug: Log when client receives position update (first few times) if not has_meta("position_sync_count"): @@ -474,7 +538,7 @@ func _sync_position(pos: Vector2, vel: Vector2, z_pos: float = 0.0, dir: int = 0 var sync_count = get_meta("position_sync_count") + 1 set_meta("position_sync_count", sync_count) - if sync_count <= 3: # Log first 3 syncs + if sync_count <= 3: # Log first 3 syncs print("Enemy ", name, " (client) received position sync #", sync_count, ": pos=", pos) # Update position and state @@ -504,7 +568,7 @@ func _sync_damage_visual(): # Clients receive damage visual sync # Only process if we're not the authority (i.e., we're a client) if is_multiplayer_authority(): - return # Server ignores its own updates + return # Server ignores its own updates _flash_damage() @@ -513,7 +577,7 @@ func _sync_death(): # Clients receive death sync and play death animation locally # Only process if we're not the authority (i.e., we're a client) if is_multiplayer_authority(): - return # Server ignores its own updates + return # Server ignores its own updates if not is_dead: is_dead = true @@ -521,7 +585,7 @@ func _sync_death(): # Remove collision layer so they don't collide with players, but still collide with walls # This matches what happens on the server when rats/slimes die - set_collision_layer_value(2, false) # Remove from enemy collision layer (layer 2) + set_collision_layer_value(2, false) # Remove from enemy collision layer (layer 2) # Immediately mark as dead and stop AI/physics # This prevents "inactive" enemies that are already dead diff --git a/src/scripts/enemy_bat.gd b/src/scripts/enemy_bat.gd index 1ed1f9a..e047b05 100644 --- a/src/scripts/enemy_bat.gd +++ b/src/scripts/enemy_bat.gd @@ -128,10 +128,10 @@ func _update_z_position(_delta): if sprite: 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: 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 func _update_client_visuals(): diff --git a/src/scripts/enemy_humanoid.gd b/src/scripts/enemy_humanoid.gd index 406c61c..6f44b5b 100644 --- a/src/scripts/enemy_humanoid.gd +++ b/src/scripts/enemy_humanoid.gd @@ -157,13 +157,243 @@ func _setup_appearance(): # Load random equipment/appearance _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 random beastkin addons (low chance) + # Load random beastkin addons (low chance, can override type addon) if appearance_rng.randf() < 0.1: # 10% chance _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: match type: HumanoidType.CYCLOPS: @@ -188,9 +418,175 @@ func _get_body_texture_for_type(type: HumanoidType) -> String: return "" func _load_random_equipment(): - # Load random shoes, armour, etc. (can be empty) - # For now, just load basic stuff - can be expanded later - pass + # Load random equipment (shoes, clothes, gloves, headgear) + # Equipment is optional - chance to have each piece + + # 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(): # Load type-specific addons based on requirements @@ -224,15 +620,9 @@ func _load_type_addons(): print(name, " loaded skeleton horns: ", addon_path) HumanoidType.HUMANOID: - # Can have (but not must) ElfEars - if appearance_rng.randf() < 0.3 and sprite_addons: - var addon_path = "res://assets/gfx/Puny-Characters/Layer 7 - Add-ons/Elf Add-ons/ElfEars1.png" - var texture = load(addon_path) - if texture: - sprite_addons.texture = texture - sprite_addons.hframes = 35 - sprite_addons.vframes = 8 - print(name, " loaded elf ears") + # Ears are already set by _randomize_appearance, so skip here + # Type-specific addons would override if needed + pass HumanoidType.NIGHTELF: # Must have NightElfEars7 diff --git a/src/scripts/enemy_spawner.gd b/src/scripts/enemy_spawner.gd index 2602e43..e9988db 100644 --- a/src/scripts/enemy_spawner.gd +++ b/src/scripts/enemy_spawner.gd @@ -10,13 +10,14 @@ extends Node2D var spawned_enemies: Array = [] var respawn_timer: float = 0.0 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(): print("========== EnemySpawner READY ==========") print(" Position: ", global_position) print(" Is server: ", multiplayer.is_server()) 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(" max_enemies: ", max_enemies) print(" enemy_scenes.size(): ", enemy_scenes.size()) @@ -45,8 +46,9 @@ func _process(delta): # Clean up dead enemies from list spawned_enemies = spawned_enemies.filter(func(e): return is_instance_valid(e) and not e.is_dead) - # Check if we need to respawn - if spawned_enemies.size() < max_enemies: + # Check if we need to respawn (only if respawn_time > 0) + # 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 if respawn_timer >= respawn_time: spawn_enemy() @@ -95,19 +97,40 @@ func spawn_enemy(): # Spawn multiple smoke puffs at slightly different positions var smoke_puffs = [] 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): var puff_offset = Vector2( 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: 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 - # 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 - var smoke_animation_duration = (4.0 / 10.0) + 1.5 + 0.5 # Total animation time + # 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.0 + 0.3 # Reduced from 2.4s to 1.7s await get_tree().create_timer(smoke_animation_duration).timeout print(" Smoke puffs finished - now spawning enemy...") @@ -126,6 +149,27 @@ func spawn_enemy(): enemy.spawn_position = 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) enemy.set_meta("spawned_from_spawner", true) enemy.set_meta("spawner_name", name) @@ -167,6 +211,7 @@ func spawn_enemy(): enemy.set_meta("spawn_scene_index", scene_index) 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(" Total spawned enemies: ", spawned_enemies.size()) @@ -191,16 +236,19 @@ func spawn_enemy(): if game_world and game_world.has_method("_sync_enemy_spawn"): # Use spawner name for identification - print(" DEBUG: Calling _sync_enemy_spawn.rpc with name=", name, " pos=", global_position, " scene_index=", scene_index) - game_world._sync_enemy_spawn.rpc(name, global_position, scene_index) - print(" Sent RPC to sync enemy spawn to clients: spawner=", name, " pos=", global_position, " scene_index=", scene_index) + # Pass humanoid_type if it's a humanoid enemy (for syncing to clients) + var sync_humanoid_type = humanoid_type if humanoid_type != null else -1 + 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: 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) -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 # 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 if scene_index >= 0 and scene_index < enemy_scenes.size(): # 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.") 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 - _spawn_smoke_puff() + # NOTE: Smoke puffs are synced via RPC (_sync_smoke_puffs) from server + # so we don't spawn them here - they're already spawned by the RPC handler # Instantiate and add enemy 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: 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) # 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) @@ -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) 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 = [] for enemy in spawned_enemies: if is_instance_valid(enemy) and not enemy.is_dead: var scene_index = -1 if enemy.has_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 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() if puff: 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: 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 else: print(" ERROR: No parent node for smoke puff!") diff --git a/src/scripts/floor_switch.gd b/src/scripts/floor_switch.gd index 5adddc2..5391f3b 100644 --- a/src/scripts/floor_switch.gd +++ b/src/scripts/floor_switch.gd @@ -68,6 +68,7 @@ func _on_body_entered(body): if object_type == "Pillar" and not is_being_held: var weight = _get_object_weight(body) if weight >= required_weight: # Pillar must have weight >= 5.0 + $PressSwitch.play() objects_on_switch.append(body) current_weight += 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()): var weight = _get_object_weight(body) if weight > 0: + $PressSwitch.play() objects_on_switch.append(body) current_weight += weight _check_activation() @@ -91,12 +93,13 @@ func _on_body_exited(body): # For pillar switches, verify the object is still valid (not being held now) if switch_type == "pillar": 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) if object_type == "Pillar": var weight = _get_object_weight(body) if weight > 0: + $ReleaseSwitch.play() objects_on_switch.erase(body) current_weight -= 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 var weight = _get_object_weight(body) if weight > 0: + $ReleaseSwitch.play() objects_on_switch.erase(body) current_weight -= weight _check_activation() diff --git a/src/scripts/game_world.gd b/src/scripts/game_world.gd index 2ca97a3..05ff7a9 100644 --- a/src/scripts/game_world.gd +++ b/src/scripts/game_world.gd @@ -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 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 # 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 # which includes dungeon_data.interactable_objects and calls _spawn_interactable_objects() on the client. - - # Note: Interactable objects are also synced via _sync_dungeon RPC - # 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. # Sync existing torches to the new client - _sync_existing_torches_to_client(peer_id) + call_deferred("_sync_existing_torches_to_client", peer_id) else: # Clients spawn directly when they receive this signal print("GameWorld: Client spawning players for peer ", peer_id) @@ -107,21 +110,31 @@ func _on_player_connected(peer_id: int, player_info: Dictionary): func _sync_existing_enemies_to_client(client_peer_id: int): # 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 = [] - for child in get_children(): - if child.has_method("get_spawned_enemy_positions") and child.has_method("spawn_enemy_at_position"): - spawners.append(child) + 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(): + 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) print("GameWorld: Syncing existing enemies to client ", client_peer_id, " from ", spawners.size(), " spawners") for spawner in spawners: var enemy_data = spawner.get_spawned_enemy_positions() 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 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) - print("GameWorld: Sent enemy spawn sync to client ", client_peer_id, ": spawner=", spawner.name, " pos=", pos, " scene_index=", scene_index) + var humanoid_type = data.humanoid_type if "humanoid_type" in data else -1 + _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): 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) @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 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) - var spawner = get_node_or_null(spawner_name) + # Find the spawner node by name (spawners are children of Entities node, not GameWorld) + 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: - 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 if not spawner.has_method("spawn_enemy_at_position"): push_error("ERROR: Spawner does not have spawn_enemy_at_position method!") return - # Call spawn method on the spawner with scene index - spawner.spawn_enemy_at_position(spawn_position, scene_index) + # Call spawn method on the spawner with scene index and humanoid_type + spawner.spawn_enemy_at_position(spawn_position, scene_index, humanoid_type) # Loot ID counter (server only) 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) @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 if multiplayer.is_server(): return # Server ignores this (it's the sender) - # Update stats before showing - level_enemies_defeated = enemies_defeated - level_times_downed = times_downed - level_exp_collected = exp_collected - level_coins_collected = coins_collected + # Stop HUD timer when level completes (on clients too) + var hud = get_node_or_null("IngameHUD") + if hud and hud.has_method("stop_timer"): + hud.stop_timer() - # Show level complete UI - _show_level_complete_ui() + # Show level complete UI (each client will show their own local player's stats) + _show_level_complete_ui(level_time) @rpc("authority", "reliable") func _sync_hide_level_complete(): @@ -501,6 +543,9 @@ func _generate_dungeon(): if multiplayer.has_multiplayer_peer(): _sync_show_level_number.rpc(current_level) + # Load HUD after dungeon generation completes (non-blocking) + call_deferred("_load_hud") + # Sync dungeon to all clients if multiplayer.has_multiplayer_peer(): # Get host's current room for spawning new players near host @@ -513,6 +558,8 @@ func _generate_dungeon(): print("GameWorld: WARNING: Server dungeon_data has NO 'enemies' key before sync!") _sync_dungeon.rpc(dungeon_data, dungeon_seed, current_level, host_room) + + print("GameWorld: Dungeon generation completed successfully") func _render_dungeon(): if dungeon_data.is_empty(): @@ -1083,6 +1130,10 @@ func _spawn_enemies(): if "damage" in enemy_data: 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) # 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) @@ -1156,7 +1207,7 @@ func _spawn_interactable_objects(): 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: push_error("ERROR: Could not load interactable_object scene!") return @@ -1290,7 +1341,10 @@ func _sync_dungeon_enemy_spawn(enemy_data: Dictionary): # Set enemy stats BEFORE adding to scene if "max_health" in enemy_data: 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: enemy.move_speed = enemy_data.move_speed 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()) +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): # Sync existing torches to newly connected client 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"): child._force_drop_held_object() - # Collect stats from all players - _collect_level_stats() + # Stop HUD timer when level completes + 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(player) - # Show level complete UI (server and clients) - _show_level_complete_ui() - # Sync to all clients + # Show level complete UI (server and clients) with per-player stats + _show_level_complete_ui(level_time) + # Sync to all clients (each client will show their own local player's stats) 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 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") _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() @@ -1537,32 +1638,40 @@ func _on_player_reached_stairs(player: Node): _sync_dungeon.rpc(dungeon_data, dungeon_seed, current_level, start_room) -func _collect_level_stats(): - # Reset stats - level_enemies_defeated = 0 - level_times_downed = 0 - level_exp_collected = 0.0 - level_coins_collected = 0 +func _get_local_player_stats() -> Dictionary: + # Get stats for the local player (for level complete screen) + # Returns a dictionary with: enemies_defeated, coins_collected + var stats = { + "enemies_defeated": 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") - for player in players: - if player.character_stats: - # Count enemies defeated (kills) - if "kills" in player.character_stats: - level_enemies_defeated += player.character_stats.kills - - # Count times downed (deaths) - if "deaths" in player.character_stats: - level_times_downed += player.character_stats.deaths - - # Collect exp - if "xp" in player.character_stats: - level_exp_collected += player.character_stats.xp - - # Collect coins - if "coin" in player.character_stats: - level_coins_collected += player.character_stats.coin + var local_player = null + + if multiplayer.has_multiplayer_peer(): + # Multiplayer: find player with matching authority + for player in players: + if player.has_method("is_multiplayer_authority") and player.is_multiplayer_authority(): + local_player = player + break + else: + # Single-player: use first player + if players.size() > 0: + local_player = players[0] + + if local_player and local_player.character_stats: + # Get enemies defeated (kills) + if "kills" in local_player.character_stats: + stats.enemies_defeated = local_player.character_stats.kills + + # Get coins collected + if "coin" in local_player.character_stats: + stats.coins_collected = local_player.character_stats.coin + + return stats func _fade_out_player(player: Node): # Fade out all sprite layers @@ -1637,7 +1746,7 @@ func _fade_in_player(player: Node): sprite_layer.modulate.a = 0.0 # Start invisible 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 var level_complete_ui = get_node_or_null("LevelCompleteUI") if not level_complete_ui: @@ -1659,11 +1768,17 @@ func _show_level_complete_ui(): if level_complete_ui: 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_enemies_defeated, - level_times_downed, - level_exp_collected, - level_coins_collected + local_stats.enemies_defeated, + 0, # times_downed - not shown per user request + 0.0, # exp_collected - not shown per user request + local_stats.coins_collected, + level_time, + current_level ) func _show_level_number(): @@ -1699,47 +1814,167 @@ func _show_level_number(): else: 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: # Create level complete UI programmatically var canvas_layer = CanvasLayer.new() canvas_layer.name = "LevelCompleteUI" 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() - vbox.set_anchors_preset(Control.PRESET_CENTER) - vbox.offset_top = -200 # Position a bit up from center + vbox.theme = theme 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() + title.name = "TitleLabel" 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.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width 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 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) - # Stats labels + # Stats labels - enemies defeated and level time var enemies_label = Label.new() enemies_label.name = "EnemiesLabel" + enemies_label.theme = theme 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) + 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() downed_label.name = "DownedLabel" + downed_label.theme = theme 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) var exp_label = Label.new() exp_label.name = "ExpLabel" + exp_label.theme = theme 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) var coins_label = Label.new() coins_label.name = "CoinsLabel" + coins_label.theme = theme 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) # Add script @@ -1810,7 +2045,7 @@ func _spawn_blocking_doors(): if blocking_doors == null or not blocking_doors is Array: return - var door_scene = preload("res://scenes/door.tscn") + var door_scene = load("res://scenes/door.tscn") if not door_scene: push_error("ERROR: Could not load door scene!") return @@ -1822,6 +2057,9 @@ func _spawn_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()): var door_data = blocking_doors[i] 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) - # 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) # room2 = other room (where door leads TO) # 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 if "room1" in original_door and 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: door.room2 = original_door.room2 @@ -1990,6 +2238,42 @@ func _spawn_blocking_doors(): door.connected_switches.append(existing_switch) 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") + + # 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: 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 @@ -2038,7 +2322,16 @@ func _spawn_blocking_doors(): # If this is a pillar switch, place a pillar in the same room if switch_type == "pillar": - _place_pillar_in_room(door_blocking_room, switch_pos) + # 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) + # 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: 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 @@ -2119,15 +2412,18 @@ func _spawn_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: - # 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) - var switch_script = load("res://scripts/floor_switch.gd") - if not switch_script: - push_error("ERROR: Could not load floor_switch script!") + var switch_scene = preload("res://scenes/floor_switch.tscn") + if not switch_scene: + 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 - var switch = Area2D.new() - switch.set_script(switch_script) switch.name = "FloorSwitch_%d_%d" % [tile_x, tile_y] 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.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 if multiplayer.has_multiplayer_peer(): switch.set_multiplayer_authority(1) @@ -2259,9 +2548,11 @@ func _spawn_room_triggers(): print("GameWorld: Spawning ", rooms.size(), " room triggers") + var triggers_spawned = 0 for i in range(rooms.size()): var room = rooms[i] if not room is Dictionary: + print("GameWorld: WARNING - Room at index ", i, " is not a Dictionary, skipping") continue var trigger = Area2D.new() @@ -2298,8 +2589,10 @@ func _spawn_room_triggers(): # Add to scene 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): # 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(): 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: push_error("ERROR: Could not load interactable_object scene for pillar!") return @@ -2366,15 +2659,22 @@ func _place_pillar_in_room(room: Dictionary, switch_position: Vector2): var tile_size = 16 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 # 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) # To exclude last tile, we want: room.x+2, room.x+3, ..., room.x+room.w-4 var min_x = room.x + 2 - var max_x = room.x + room.w - 4 # Exclude last 2 floor tiles (room.w-3 is last floor, room.w-4 is second-to-last) + var max_x = room.x + room.w - 4 # Exclude last 2 floor tiles (room.w-3 is last floor, room.w-4 is second-to-last) 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 y in range(min_y, max_y + 1): @@ -2388,15 +2688,22 @@ func _place_pillar_in_room(room: Dictionary, switch_position: Vector2): var world_y = y * tile_size + 8 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) - 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) + 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: - # 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() - 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()] # Spawn pillar interactable object diff --git a/src/scripts/interactable_object.gd b/src/scripts/interactable_object.gd index 3f9387f..032244f 100644 --- a/src/scripts/interactable_object.gd +++ b/src/scripts/interactable_object.gd @@ -270,7 +270,14 @@ func _break_into_pieces(): tp.angular_velocity = randf_range(-7, 7) 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 queue_free() @@ -294,13 +301,14 @@ func on_grabbed(by_player): # In multiplayer, send RPC to server if client is opening if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority(): # 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() + print("Chest: Client sending RPC to open chest, player_peer_id: ", player_peer_id) _request_chest_open.rpc_id(1, player_peer_id) else: # Server or single player - open directly _open_chest(by_player) - return + return # CRITICAL: Return early to prevent normal grab behavior is_being_held = true held_by_player = by_player @@ -434,7 +442,20 @@ func setup_chest(): var chest_frames = [12, 31] 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_opened_frame = opened_frames[index] @@ -478,7 +499,7 @@ func _open_chest(by_player: Node = null): # Only process on server (authority) if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority(): return - + $SfxOpenChest.play() is_chest_opened = true if sprite and chest_opened_frame >= 0: sprite.frame = chest_opened_frame @@ -502,34 +523,34 @@ func _open_chest(by_player: Node = null): by_player.add_coins(1) # Show pickup notification with coin graphic 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": var heal_amount = 20.0 if by_player.has_method("heal"): by_player.heal(heal_amount) # Show pickup notification with apple graphic 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": var heal_amount = 20.0 if by_player.has_method("heal"): by_player.heal(heal_amount) # Show pickup notification with banana graphic 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": var heal_amount = 20.0 if by_player.has_method("heal"): by_player.heal(heal_amount) # Show pickup notification with cherry graphic 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": if by_player.has_method("add_key"): by_player.add_key(1) # Show pickup notification with key graphic 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 if has_node("SfxChestOpen"): @@ -549,7 +570,10 @@ func _request_chest_open(player_peer_id: int): if not multiplayer.is_server(): return + print("Chest: Server received RPC to open chest, player_peer_id: ", player_peer_id) + if is_chest_opened: + print("Chest: Chest already opened, ignoring request") return # 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!") 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(player) @@ -588,3 +613,24 @@ func _show_item_pickup_notification(player: Node, text: String, text_color: Colo parent.add_child(floating_text) 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 + +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 diff --git a/src/scripts/level_complete_ui.gd b/src/scripts/level_complete_ui.gd index 485c171..463487b 100644 --- a/src/scripts/level_complete_ui.gd +++ b/src/scripts/level_complete_ui.gd @@ -3,32 +3,46 @@ extends CanvasLayer # Level Complete UI - Shows stats when level is completed var title_label: Label = null +var stats_header_label: Label = null var enemies_label: Label = null +var time_label: Label = null var downed_label: Label = null var exp_label: Label = null var coins_label: Label = null func _ready(): visible = false + _find_labels() + +func _find_labels(): # Find labels (works for both scene-based and programmatically created UI) var vbox = get_child(0) if get_child_count() > 0 else null if vbox: for child in vbox.get_children(): - if child is Label and child.text == "LEVEL COMPLETE!": - title_label = child - elif child.name == "EnemiesLabel": - enemies_label = child - elif child.name == "DownedLabel": - downed_label = child - elif child.name == "ExpLabel": - exp_label = child - elif child.name == "CoinsLabel": - coins_label = child + 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 + 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": + enemies_label = child + elif child.name == "TimeLabel": + time_label = child + elif child.name == "DownedLabel": + downed_label = child + elif child.name == "ExpLabel": + exp_label = child + elif child.name == "CoinsLabel": + coins_label = child elif child is VBoxContainer: # Stats container for stat_child in child.get_children(): if stat_child.name == "EnemiesLabel": enemies_label = stat_child + elif stat_child.name == "TimeLabel": + time_label = stat_child elif stat_child.name == "DownedLabel": downed_label = stat_child elif stat_child.name == "ExpLabel": @@ -36,22 +50,48 @@ func _ready(): elif stat_child.name == "CoinsLabel": 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 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: - 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: + downed_label.visible = false downed_label.text = "Times Downed: " + str(times_downed) if exp_label: + exp_label.visible = false exp_label.text = "EXP Collected: " + str(int(exp_collected)) 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 visible = true @@ -68,9 +108,15 @@ func show_stats(enemies_defeated: int, times_downed: int, exp_collected: float, if title_label: title_label.modulate.a = 0.0 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: enemies_label.modulate.a = 0.0 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: downed_label.modulate.a = 0.0 fade_in.tween_property(downed_label, "modulate:a", 1.0, 0.5) diff --git a/src/scripts/loot.gd b/src/scripts/loot.gd index 7aeeeeb..b02fc51 100644 --- a/src/scripts/loot.gd +++ b/src/scripts/loot.gd @@ -38,6 +38,7 @@ var collected: bool = false @onready var sfx_coin_collect = $SfxCoinCollect @onready var sfx_loot_collect = $SfxLootCollect @onready var sfx_potion_collect = $SfxPotionCollect +@onready var sfx_banana_collect = $SfxBananaCollect @onready var sfx_key_collect = $SfxKeyCollect func _ready(): @@ -102,21 +103,21 @@ func _setup_sprite(): sprite.texture = items_texture sprite.hframes = 20 sprite.vframes = 14 - sprite.frame = (8 * 20) + 11 + sprite.frame = (8 * 20) + 10 LootType.BANANA: var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") if items_texture: sprite.texture = items_texture sprite.hframes = 20 sprite.vframes = 14 - sprite.frame = (8 * 20) + 12 + sprite.frame = (8 * 20) + 11 LootType.CHERRY: var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") if items_texture: sprite.texture = items_texture sprite.hframes = 20 sprite.vframes = 14 - sprite.frame = (8 * 20) + 13 + sprite.frame = (8 * 20) + 12 LootType.KEY: var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") if items_texture: @@ -165,6 +166,14 @@ func _physics_process(delta): # Apply gravity to Z-axis acceleration_z = -300.0 # Gravity 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 # 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) 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: # Bounce on floor - if loot_type == LootType.COIN and bounce_timer == 0.0: - # Play bounce sound for coins + # Only play bounce sound if bounce is significant enough and timer has elapsed + # 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: - # Adjust volume based on bounce velocity - sfx_coin_bounce.volume_db = -1.0 + (-10.0 - (velocity_z * 0.1)) + # Adjust volume based on bounce velocity (softer for smaller bounces) + 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() - 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 - is_airborne = true # Still bouncing + + # 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 else: + # Velocity too small or collected - stop bouncing velocity_z = 0.0 is_airborne = false else: is_airborne = false + # Ensure velocity_z is zero when on ground + velocity_z = 0.0 # Apply friction even when not airborne (on ground) 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) # Show floating text with item graphic and text 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 @@ -358,7 +381,7 @@ func _process_pickup_on_server(player: Node): player.heal(heal_amount) # Show floating text with item graphic and heal amount 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 @@ -367,8 +390,8 @@ func _process_pickup_on_server(player: Node): await sfx_potion_collect.finished queue_free() LootType.BANANA: - if sfx_potion_collect: - sfx_potion_collect.play() + if sfx_banana_collect: + sfx_banana_collect.play() # Heal player var actual_heal = 0.0 if player.has_method("heal"): @@ -376,17 +399,17 @@ func _process_pickup_on_server(player: Node): player.heal(heal_amount) # Show floating text with item graphic and heal amount 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 # Wait for sound to finish before removing - if sfx_potion_collect and sfx_potion_collect.playing: - await sfx_potion_collect.finished + if sfx_banana_collect and sfx_banana_collect.playing: + await sfx_banana_collect.finished queue_free() LootType.CHERRY: - if sfx_potion_collect: - sfx_potion_collect.play() + if sfx_banana_collect: + sfx_banana_collect.play() # Heal player var actual_heal = 0.0 if player.has_method("heal"): @@ -394,13 +417,13 @@ func _process_pickup_on_server(player: Node): player.heal(heal_amount) # Show floating text with item graphic and heal amount 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 # Wait for sound to finish before removing - if sfx_potion_collect and sfx_potion_collect.playing: - await sfx_potion_collect.finished + if sfx_banana_collect and sfx_banana_collect.playing: + await sfx_banana_collect.finished queue_free() LootType.KEY: if sfx_key_collect: @@ -410,7 +433,7 @@ func _process_pickup_on_server(player: Node): player.add_key(1) # Show floating text with item graphic and text 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 diff --git a/src/scripts/player.gd b/src/scripts/player.gd index ae6d173..651a5a4 100644 --- a/src/scripts/player.gd +++ b/src/scripts/player.gd @@ -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 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 teleported_this_frame: bool = false # Flag to prevent position sync from overriding teleportation # Input device (for local multiplayer) 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_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 was_dragging_last_frame = false # Track if we were dragging last frame to detect start/stop # Being held state var being_held_by: Node = null @@ -720,6 +722,9 @@ func _update_z_physics(delta): shadow.modulate.a = 0.5 func _physics_process(delta): + # Reset teleport flag at start of frame + teleported_this_frame = false + # Update animations _update_animation(delta) @@ -963,6 +968,27 @@ func _handle_input(): if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD": _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 var current_speed = move_speed * (0.5 if is_pushing else 1.0) velocity = input_vector * current_speed @@ -1263,6 +1289,7 @@ func _lift_object(): _sync_grab.rpc(held_object.get_path(), true, push_axis) print("Lifted: ", held_object.name) + $SfxLift.play() func _start_pushing(): if not held_object: @@ -1321,6 +1348,11 @@ func _stop_pushing(): is_pushing = false 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! var released_obj = held_object var released_obj_position = released_obj.global_position # Store exact position @@ -1443,6 +1475,7 @@ func _throw_object(): # Play throw animation _set_animation("THROW") + $SfxThrow.play() # Sync throw over network 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"): 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") func _sync_held_object_pos(obj_path: NodePath, pos: Vector2): # 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) damage_label.global_position = global_position + Vector2(0, -16) +@rpc("any_peer", "reliable") func _sync_damage(_amount: float, attacker_position: Vector2): # This RPC only syncs visual effects, not damage application # (damage is already applied via rpc_take_damage) diff --git a/src/scripts/room_trigger.gd b/src/scripts/room_trigger.gd index e966377..9dac26b 100644 --- a/src/scripts/room_trigger.gd +++ b/src/scripts/room_trigger.gd @@ -21,12 +21,12 @@ func _ready(): body_entered.connect(_on_body_entered) 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) # This ensures doors have their blocking_room set before we try to find them 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): # Player entered the room @@ -209,7 +209,7 @@ func _find_room_entities(): 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(): if child.is_in_group("enemy"): # 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: enemies_in_room.append(child) - # Find floor switches - for child in get_tree().get_nodes_in_group("floor_switch"): - if is_instance_valid(child): + # Find floor switches (search in Entities node, not all nodes in scene) + # This is more efficient and avoids searching the entire scene tree + 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 var tile_size = 16 var switch_tile_x = int(child.global_position.x / tile_size) @@ -247,8 +248,11 @@ func _find_room_entities(): switch_tile_y >= room_min_y and switch_tile_y < room_max_y: floor_switches_in_room.append(child) - # Update debug label after finding all entities - call_deferred("_update_debug_label") + 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") func _spawn_room_enemies(): print("RoomTrigger: ===== _spawn_room_enemies() CALLED for room (", room.x, ", ", room.y, ") =====") @@ -422,6 +426,10 @@ func _find_room_spawners(): func _create_debug_label(): # 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() label.name = "DebugLabel" @@ -440,17 +448,16 @@ func _create_debug_label(): label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART # 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 ResourceLoader.load() to ensure we get the FontFile resource - var standard_font_resource = ResourceLoader.load("res://assets/fonts/standard_font.png", "FontFile") - if standard_font_resource and standard_font_resource is FontFile: - label.add_theme_font_override("font", standard_font_resource) - 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!") + # Use try-catch-like approach - if font loading fails, just skip it + # This prevents blocking the game if font resource has issues + var standard_font_resource = null + 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) + + # Always set font size + label.add_theme_font_size_override("font_size", 8) # Style the label label.add_theme_color_override("font_color", Color.YELLOW) diff --git a/src/scripts/stairs.gd b/src/scripts/stairs.gd index f75148b..6c42a79 100644 --- a/src/scripts/stairs.gd +++ b/src/scripts/stairs.gd @@ -2,6 +2,8 @@ extends Area2D # Stairs that trigger level completion when player enters +@onready var sfx_stairs = null + func _ready(): # Connect body entered signal body_entered.connect(_on_body_entered) @@ -9,11 +11,30 @@ func _ready(): # Set collision layer/mask to detect players collision_layer = 0 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): 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: 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 if multiplayer.is_server() or not multiplayer.has_multiplayer_peer(): print("Stairs: Server detected, calling game_world") @@ -25,4 +46,3 @@ func _on_body_entered(body: Node2D): print("Stairs: ERROR - Game world not found!") else: print("Stairs: Not server, ignoring") - diff --git a/src/scripts/sword_projectile.gd b/src/scripts/sword_projectile.gd index 7269fbd..51c92ad 100644 --- a/src/scripts/sword_projectile.gd +++ b/src/scripts/sword_projectile.gd @@ -14,7 +14,7 @@ var travel_direction: Vector2 = Vector2.RIGHT var elapsed_time: float = 0.0 var distance_traveled: float = 0.0 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 hit_area = $Area2D @@ -66,7 +66,7 @@ func _on_body_entered(body): if body == player_owner: 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: return @@ -77,7 +77,8 @@ func _on_body_entered(body): if player_owner and not player_owner.is_multiplayer_authority(): 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 # 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 elif "health" in body: - $SfxImpact.play() + #$SfxImpact.play() # Boxes have health property body.health -= damage if body.health <= 0: diff --git a/src/scripts/sword_slash.gd b/src/scripts/sword_slash.gd index d36d0a9..ca3fe62 100644 --- a/src/scripts/sword_slash.gd +++ b/src/scripts/sword_slash.gd @@ -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 elapsed_time: float = 0.0 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 hit_area = $Area2D @@ -57,11 +57,12 @@ func _on_body_entered(body): if body == player_owner: 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: 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 if body.is_in_group("player") and body.has_method("take_damage"): diff --git a/src/scripts/teleporter_into_closed_room.gd b/src/scripts/teleporter_into_closed_room.gd index 057a4de..b5ad5de 100644 --- a/src/scripts/teleporter_into_closed_room.gd +++ b/src/scripts/teleporter_into_closed_room.gd @@ -8,26 +8,28 @@ func _ready() -> void: # Called every frame. 'delta' is the elapsed time since the previous frame. -func _process(delta: float) -> void: +func _process(_delta: float) -> void: pass func _on_area_which_teleports_player_into_room_body_entered(body: Node2D) -> void: if !is_enabled: return - # TODO: teleport player passed the door... + # TODO: add some kind of animation + if (!$TeleportSfx.playing): + $TeleportSfx.play() get_parent().teleportPlayer(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: return $GPUParticles2D.emitting = true 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: return $GPUParticles2D.emitting = false