delete files in nickes
This commit is contained in:
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
@@ -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
|
||||
@@ -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"]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,7 +355,9 @@ func _open():
|
||||
else:
|
||||
push_error("Door: StoneDoor/GateDoor _open() called but closed_position is zero!")
|
||||
return
|
||||
|
||||
if type == "GateDoor":
|
||||
$SfxOpenGateDoor.play()
|
||||
else:
|
||||
$SfxOpenStoneDoor.play()
|
||||
|
||||
# CRITICAL: Store starting position for animation (should be closed_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,12 +430,19 @@ func _close():
|
||||
animation_start_position = position
|
||||
|
||||
print("Door: Starting close animation from ", animation_start_position, " to ", closed_position, " (offset: ", open_offset, ")")
|
||||
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
|
||||
# NEW LOGIC: Door is positioned at OPEN tile position by game_world
|
||||
@@ -646,27 +682,77 @@ func _on_room_exited(body):
|
||||
# Doors stay in their current state
|
||||
|
||||
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,9 +804,246 @@ 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
|
||||
# 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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,15 +764,58 @@ func _place_torches_in_room(room: Dictionary, grid: Array, all_doors: Array, _ma
|
||||
# We need to check all valid Y positions on the left/right walls and place at torch_y_from_floor
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
|
||||
@@ -761,16 +824,60 @@ func _place_torches_in_room(room: Dictionary, grid: Array, all_doors: Array, _ma
|
||||
# Right wall has two parts: room.x + room.w - 2 (left part, actual wall) and room.x + room.w - 1 (right part, also corner)
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
|
||||
@@ -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,13 +1901,47 @@ func _place_interactable_objects_in_room(room: Dictionary, grid: Array, map_size
|
||||
if _is_valid_interactable_position(world_pos, all_doors, all_enemies, room):
|
||||
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
|
||||
# CRITICAL: If room has pillar switch puzzle, the FIRST object MUST be a Pillar
|
||||
if has_pillar_switch_puzzle:
|
||||
# Place pillar as the first object
|
||||
var pillar_type_data = {"type": "Pillar", "setup": "setup_pillar"}
|
||||
objects.append({
|
||||
"type": pillar_type_data.type,
|
||||
"setup_function": pillar_type_data.setup,
|
||||
"position": valid_positions[0],
|
||||
"room": room
|
||||
})
|
||||
|
||||
# Place remaining objects (skip first position since it's used by the pillar)
|
||||
# We need to place num_objects - 1 more objects (since pillar counts as 1)
|
||||
var remaining_objects = num_objects - 1
|
||||
var positions_index = 1 # Start from second position
|
||||
for i in range(min(remaining_objects, valid_positions.size() - 1)):
|
||||
if positions_index >= valid_positions.size():
|
||||
break # No more valid positions
|
||||
var object_type_data = object_types[rng.randi() % object_types.size()]
|
||||
# Skip Pillar type for remaining objects (already placed one)
|
||||
while object_type_data.type == "Pillar":
|
||||
object_type_data = object_types[rng.randi() % object_types.size()]
|
||||
|
||||
objects.append({
|
||||
"type": object_type_data.type,
|
||||
"setup_function": object_type_data.setup,
|
||||
"position": valid_positions[positions_index],
|
||||
"room": room
|
||||
})
|
||||
positions_index += 1
|
||||
else:
|
||||
# Normal placement: no pillar requirement
|
||||
for i in range(min(num_objects, valid_positions.size())):
|
||||
var object_type_data = object_types[rng.randi() % object_types.size()]
|
||||
var position = valid_positions[i]
|
||||
@@ -1872,7 +2070,7 @@ func _find_rooms_before_door(door: Dictionary, start_room: Dictionary, _all_room
|
||||
|
||||
return rooms_before_door
|
||||
|
||||
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 \
|
||||
var room_matches = false
|
||||
|
||||
# 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)
|
||||
|
||||
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
|
||||
# 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)
|
||||
|
||||
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
|
||||
# Door is connected to puzzle room
|
||||
if room_matches:
|
||||
doors_in_room.append(door)
|
||||
|
||||
# Skip if door leads into start or exit room
|
||||
if door_room2_index == start_room_index or door_room2_index == exit_room_index:
|
||||
continue
|
||||
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
|
||||
|
||||
doors_out_of_room.append(door)
|
||||
|
||||
if doors_out_of_room.size() == 0:
|
||||
continue # No doors leading out of 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 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)
|
||||
"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 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 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 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 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 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 middle column, top row
|
||||
# open_tile_x is already on the wall, adjust to middle column
|
||||
open_tile_x = open_tile_x + 1 # Middle column (3 tiles wide, so +1 from left edge)
|
||||
open_tile_y = open_tile_y + 0 # Already at top wall (row 0)
|
||||
"Right":
|
||||
# Door Right (2x3): Open at right column, middle row
|
||||
# open_tile_x is already on the wall, adjust to right column
|
||||
open_tile_x = open_tile_x + 1 # Right column (already at wall)
|
||||
open_tile_y = open_tile_y + 1 # Middle row (3 tiles tall, so +1 from top)
|
||||
"Down":
|
||||
# Door Down (3x2): Open at middle column, bottom row
|
||||
# open_tile_x is already on the wall, adjust to middle column
|
||||
open_tile_x = open_tile_x + 1 # Middle column (3 tiles wide, so +1 from left edge)
|
||||
open_tile_y = open_tile_y + 1 # Bottom row (2 tiles tall, so +1 from top edge)
|
||||
"Left":
|
||||
# Door Left (2x3): Open at left column, middle row
|
||||
# open_tile_x is already on the wall, adjust to left column
|
||||
open_tile_x = open_tile_x + 0 # Left column (already at wall)
|
||||
open_tile_y = open_tile_y + 1 # Middle row (3 tiles tall, so +1 from top)
|
||||
|
||||
# Calculate world position from open tile (center of tile)
|
||||
# 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
|
||||
|
||||
@@ -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
|
||||
@@ -169,7 +170,7 @@ func _check_interactable_object_collision():
|
||||
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
|
||||
@@ -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,9 +305,21 @@ 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):
|
||||
# RPC version - only process on server/authority
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,9 +110,18 @@ 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 = []
|
||||
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.has_method("get_spawned_enemy_positions") and child.has_method("spawn_enemy_at_position"):
|
||||
if child.is_in_group("enemy_spawner") or (child.has_method("get_spawned_enemy_positions") and child.has_method("spawn_enemy_at_position")):
|
||||
spawners.append(child)
|
||||
|
||||
print("GameWorld: Syncing existing enemies to client ", client_peer_id, " from ", spawners.size(), " spawners")
|
||||
@@ -117,11 +129,12 @@ func _sync_existing_enemies_to_client(client_peer_id: int):
|
||||
for spawner in spawners:
|
||||
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
|
||||
@@ -514,6 +559,8 @@ func _generate_dungeon():
|
||||
|
||||
_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():
|
||||
push_error("ERROR: Cannot render dungeon - no dungeon data!")
|
||||
@@ -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")
|
||||
var local_player = null
|
||||
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
# Multiplayer: find player with matching authority
|
||||
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
|
||||
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]
|
||||
|
||||
# Count times downed (deaths)
|
||||
if "deaths" in player.character_stats:
|
||||
level_times_downed += player.character_stats.deaths
|
||||
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
|
||||
|
||||
# Collect exp
|
||||
if "xp" in player.character_stats:
|
||||
level_exp_collected += player.character_stats.xp
|
||||
# Get coins collected
|
||||
if "coin" in local_player.character_stats:
|
||||
stats.coins_collected = local_player.character_stats.coin
|
||||
|
||||
# Collect coins
|
||||
if "coin" in player.character_stats:
|
||||
level_coins_collected += 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":
|
||||
# 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,7 +2659,8 @@ 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)
|
||||
@@ -2376,6 +2670,12 @@ func _place_pillar_in_room(room: Dictionary, switch_position: Vector2):
|
||||
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 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):
|
||||
if x >= 0 and x < dungeon_data.map_size.x and y >= 0 and y < dungeon_data.map_size.y:
|
||||
@@ -2388,15 +2688,22 @@ func _place_pillar_in_room(room: Dictionary, switch_position: Vector2):
|
||||
var world_y = y * tile_size + 8
|
||||
var world_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
|
||||
|
||||
@@ -271,6 +271,13 @@ func _break_into_pieces():
|
||||
|
||||
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
|
||||
|
||||
@@ -3,21 +3,33 @@ 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!":
|
||||
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":
|
||||
@@ -29,6 +41,8 @@ func _ready():
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -21,13 +21,13 @@ 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
|
||||
if body.is_in_group("player"):
|
||||
@@ -209,7 +209,7 @@ func _find_room_entities():
|
||||
|
||||
print("RoomTrigger: Found ", doors_in_room.size(), " doors for room (", room.x, ", ", room.y, ")")
|
||||
|
||||
# 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,7 +248,10 @@ 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
|
||||
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():
|
||||
@@ -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:
|
||||
# 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)
|
||||
else:
|
||||
# Fallback: just set smaller font size if font can't be loaded
|
||||
label.add_theme_font_size_override("font_size", 8)
|
||||
if not standard_font_resource:
|
||||
print("RoomTrigger: WARNING - Could not load standard_font.png font resource!")
|
||||
|
||||
# Style the label
|
||||
label.add_theme_color_override("font_color", Color.YELLOW)
|
||||
|
||||
@@ -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)
|
||||
@@ -10,10 +12,29 @@ func _ready():
|
||||
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")
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user