From 25be2c00bd9f61364a6fcd48ea55e0063fa92b76 Mon Sep 17 00:00:00 2001 From: Elrinth Date: Sat, 10 Jan 2026 19:46:55 +0100 Subject: [PATCH] added blocking doors to paths. --- .../sfx/environment/keydoor/door_closes.mp3 | Bin 0 -> 16843 bytes .../keydoor/door_closes.mp3.import | 19 + src/assets/gfx/RPG DUNGEON VOL 3.png | Bin 16925 -> 17460 bytes src/assets/gfx/pickups/items_n_shit.png | Bin 47149 -> 44696 bytes src/scenes/TeleporterIntoClosedRoom.tscn | 114 ++ src/scenes/door.tscn | 10 + src/scenes/door.tscn3989767106.tmp | 43 + src/scenes/floating_text.tscn | 12 +- src/scenes/game_world.tscn | 38 - src/scenes/smoke_puff.tscn | 10 +- src/scripts/damage_number.gd | 46 +- src/scripts/door.gd | 801 ++++++++++- src/scripts/dungeon_generator.gd | 1240 ++++++++++++++--- src/scripts/enemy_base.gd | 105 ++ src/scripts/enemy_bat.gd | 3 + src/scripts/enemy_humanoid.gd | 86 +- src/scripts/enemy_rat.gd | 3 + src/scripts/enemy_slime.gd | 10 +- src/scripts/enemy_spawner.gd | 151 +- src/scripts/floating_text.gd | 88 +- src/scripts/floor_switch.gd | 256 ++++ src/scripts/floor_switch.gd.uid | 1 + src/scripts/game_world.gd | 769 +++++++++- src/scripts/interactable_object.gd | 146 +- src/scripts/level_text_ui.gd | 6 +- src/scripts/loot.gd | 88 +- src/scripts/player.gd | 60 +- src/scripts/room_trigger.gd | 605 ++++++++ src/scripts/room_trigger.gd.uid | 1 + src/scripts/smoke_puff.gd | 84 +- src/scripts/sword_projectile.gd | 8 +- src/scripts/teleporter_into_closed_room.gd | 34 + .../teleporter_into_closed_room.gd.uid | 1 + 33 files changed, 4383 insertions(+), 455 deletions(-) create mode 100644 src/assets/audio/sfx/environment/keydoor/door_closes.mp3 create mode 100644 src/assets/audio/sfx/environment/keydoor/door_closes.mp3.import create mode 100644 src/scenes/TeleporterIntoClosedRoom.tscn create mode 100644 src/scenes/door.tscn3989767106.tmp create mode 100644 src/scripts/floor_switch.gd create mode 100644 src/scripts/floor_switch.gd.uid create mode 100644 src/scripts/room_trigger.gd create mode 100644 src/scripts/room_trigger.gd.uid create mode 100644 src/scripts/teleporter_into_closed_room.gd create mode 100644 src/scripts/teleporter_into_closed_room.gd.uid diff --git a/src/assets/audio/sfx/environment/keydoor/door_closes.mp3 b/src/assets/audio/sfx/environment/keydoor/door_closes.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..5266bd0a8563c95271a2a94f02956bef21ec6361 GIT binary patch literal 16843 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 I+Svbp0E-RMM*si- delta 15720 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

?b~C4vJOq(&9)YbIAS-kJx?&3Pg_mlr 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 Yr!0%i@84xu2n75n$f&~0q)a~jA05p*w*UYD literal 47149 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 VUF<3}_Tv!nr6i{&TOkb#{6EgiCj$Tg diff --git a/src/scenes/TeleporterIntoClosedRoom.tscn b/src/scenes/TeleporterIntoClosedRoom.tscn new file mode 100644 index 0000000..67923c7 --- /dev/null +++ b/src/scenes/TeleporterIntoClosedRoom.tscn @@ -0,0 +1,114 @@ +[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"] + +[sub_resource type="Gradient" id="Gradient_skeae"] +offsets = PackedFloat32Array(0, 0.9883721) +colors = PackedColorArray(1, 1, 1, 1, 1, 1, 1, 0) + +[sub_resource type="GradientTexture2D" id="GradientTexture2D_g3s7f"] +gradient = SubResource("Gradient_skeae") +width = 32 +height = 32 +fill = 1 +fill_from = Vector2(0.4957265, 0.4871795) +fill_to = Vector2(0.525641, 0.42307693) + +[sub_resource type="Curve" id="Curve_7twcj"] +_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(0.74757284, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0] +point_count = 3 + +[sub_resource type="CurveTexture" id="CurveTexture_sgnaw"] +curve = SubResource("Curve_7twcj") + +[sub_resource type="Gradient" id="Gradient_iemw3"] +colors = PackedColorArray(1, 1, 1, 1, 1, 1, 1, 1) + +[sub_resource type="GradientTexture1D" id="GradientTexture1D_paieq"] +gradient = SubResource("Gradient_iemw3") + +[sub_resource type="Gradient" id="Gradient_1qst8"] +colors = PackedColorArray(1, 1, 1, 1, 1, 1, 1, 1) + +[sub_resource type="GradientTexture1D" id="GradientTexture1D_cqcet"] +gradient = SubResource("Gradient_1qst8") + +[sub_resource type="Curve" id="Curve_7kx0f"] +_limits = [-1.0, 1.0, 0.0, 1.0] +_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1e-05, -0.11255813), 0.0, 0.0, 0, 0, Vector2(0.055825245, -0.43906975), 0.0, 0.0, 0, 0, Vector2(0.1553398, 0.58883727), 0.0, 0.0, 0, 0, Vector2(0.30339807, -0.2697674), 0.0, 0.0, 0, 0, Vector2(0.44902915, 0.73395354), 0.0, 0.0, 0, 0, Vector2(0.657767, -0.13674414), 0.0, 0.0, 0, 0, Vector2(0.8009709, -0.4511627), 0.0, 0.0, 0, 0] +point_count = 8 + +[sub_resource type="CurveTexture" id="CurveTexture_g8xsh"] +curve = SubResource("Curve_7kx0f") + +[sub_resource type="Curve" id="Curve_d4f27"] +_data = [Vector2(0, 0.4134884), 0.0, 0.0, 0, 0, Vector2(0.2354369, 0.49813956), 0.0, 0.0, 0, 0, Vector2(0.4830097, 0.18976748), 0.0, 0.0, 0, 0, Vector2(0.5873787, 0.8186047), 0.0, 0.0, 0, 0, Vector2(1, 1), 0.0, 0.0, 0, 0] +point_count = 5 + +[sub_resource type="CurveTexture" id="CurveTexture_nhcxx"] +curve = SubResource("Curve_d4f27") + +[sub_resource type="Curve" id="Curve_l8b8y"] +_limits = [0.0, 1000.0, 0.0, 1000.0] +_data = [Vector2(0, 413.48828), 0.0, 0.0, 0, 0, Vector2(67.96117, 467.90692), 0.0, 0.0, 0, 0, Vector2(189.3204, 213.95343), 0.0, 0.0, 0, 0, Vector2(269.41748, 50.69763), 0.0, 0.0, 0, 0, Vector2(456.31067, 280.46503), 0.0, 0.0, 0, 0] +point_count = 5 + +[sub_resource type="CurveTexture" id="CurveTexture_hd4yi"] +curve = SubResource("Curve_l8b8y") + +[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_n1yim"] +particle_flag_disable_z = true +direction = Vector3(0, -1, 0) +spread = 6.844 +initial_velocity_min = 50.76 +initial_velocity_max = 65.99 +angular_velocity_min = -36.550014 +angular_velocity_max = 21.929983 +orbit_velocity_min = -0.77199996 +orbit_velocity_max = 0.5480001 +orbit_velocity_curve = SubResource("CurveTexture_g8xsh") +radial_velocity_min = -2.2351742e-05 +radial_velocity_max = 101.51997 +radial_velocity_curve = SubResource("CurveTexture_nhcxx") +velocity_limit_curve = SubResource("CurveTexture_hd4yi") +gravity = Vector3(0, 0, 0) +color = Color(0.61982733, 0.94004476, 1, 1) +color_ramp = SubResource("GradientTexture1D_cqcet") +color_initial_ramp = SubResource("GradientTexture1D_paieq") +alpha_curve = SubResource("CurveTexture_sgnaw") +hue_variation_min = -0.06000002 +hue_variation_max = 0.049999975 + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_pp12y"] +size = Vector2(16, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_7twcj"] +size = Vector2(32, 32) + +[node name="TeleporterIntoClosedRoom" type="Node2D" unique_id=1871154484] +script = ExtResource("1_g3s7f") + +[node name="GPUParticles2D" type="GPUParticles2D" parent="." unique_id=1725884303] +emitting = false +amount = 32 +texture = SubResource("GradientTexture2D_g3s7f") +lifetime = 0.54 +randomness = 1.0 +process_material = SubResource("ParticleProcessMaterial_n1yim") + +[node name="AreaWhichTeleportsPlayerIntoRoom" type="Area2D" parent="." unique_id=47060921] +collision_mask = 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 + +[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) + +[connection signal="body_entered" from="AreaWhichTeleportsPlayerIntoRoom" to="." method="_on_area_which_teleports_player_into_room_body_entered"] +[connection signal="body_entered" from="AreaToStartEmit" to="." method="_on_area_to_start_emit_body_entered"] +[connection signal="body_exited" from="AreaToStartEmit" to="." method="_on_area_to_start_emit_body_exited"] diff --git a/src/scenes/door.tscn b/src/scenes/door.tscn index be4e64a..d07e9cd 100644 --- a/src/scenes/door.tscn +++ b/src/scenes/door.tscn @@ -2,8 +2,10 @@ [ext_resource type="Texture2D" uid="uid://cckiqfs0kwuuh" path="res://assets/gfx/door_barred.png" id="1_hpvv5"] [ext_resource type="Script" uid="uid://do4062ppepheo" path="res://scripts/door.gd" id="1_uvdjg"] +[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"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_uvdjg"] size = Vector2(26, 14) @@ -15,6 +17,9 @@ size = Vector2(22, 18) collision_layer = 64 script = ExtResource("1_uvdjg") +[node name="TeleporterIntoClosedRoom" parent="." unique_id=1871154484 instance=ExtResource("2_q5w8r")] +position = Vector2(0, -16) + [node name="Sprite2D" type="Sprite2D" parent="." unique_id=1520856168] texture = ExtResource("1_hpvv5") @@ -35,3 +40,8 @@ stream = ExtResource("4_18pbm") [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 diff --git a/src/scenes/door.tscn3989767106.tmp b/src/scenes/door.tscn3989767106.tmp new file mode 100644 index 0000000..1b35772 --- /dev/null +++ b/src/scenes/door.tscn3989767106.tmp @@ -0,0 +1,43 @@ +[gd_scene format=3 uid="uid://02opigrv0qff"] + +[ext_resource type="Texture2D" uid="uid://cckiqfs0kwuuh" path="res://assets/gfx/door_barred.png" id="1_hpvv5"] +[ext_resource type="Script" uid="uid://do4062ppepheo" path="res://scripts/door.gd" id="1_uvdjg"] +[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"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_uvdjg"] +size = Vector2(26, 14) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_la1wf"] +size = Vector2(22, 18) + +[node name="Door" type="StaticBody2D" unique_id=371155975] +collision_layer = 64 +script = ExtResource("1_uvdjg") + +[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1520856168] +texture = ExtResource("1_hpvv5") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=1691515105] +shape = SubResource("RectangleShape2D_uvdjg") + +[node name="SfxOpenKeyDoor" type="AudioStreamPlayer2D" parent="." unique_id=47303726] +stream = ExtResource("3_la1wf") + +[node name="SfxOpenStoneDoor" type="AudioStreamPlayer2D" parent="." unique_id=885417421] +stream = ExtResource("4_18pbm") + +[node name="SfxOpenGateDoor" type="AudioStreamPlayer2D" parent="." unique_id=442358170] +stream = ExtResource("4_18pbm") + +[node name="KeyInteractionArea" type="Area2D" parent="." unique_id=982067740] + +[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 diff --git a/src/scenes/floating_text.tscn b/src/scenes/floating_text.tscn index 578506d..b710dec 100644 --- a/src/scenes/floating_text.tscn +++ b/src/scenes/floating_text.tscn @@ -1,14 +1,24 @@ [gd_scene format=3 uid="uid://floating_text"] [ext_resource type="Script" path="res://scripts/floating_text.gd" id="1"] +[ext_resource type="FontFile" uid="uid://cbmcfue0ek0tk" path="res://assets/fonts/dmg_numbers.png" id="2_dmg_font"] + +[sub_resource type="Theme" id="Theme_floating_text"] +default_font = ExtResource("2_dmg_font") +default_font_size = 12 [node name="FloatingText" type="Node2D"] script = ExtResource("1") +[node name="ItemSprite" type="Sprite2D" parent="."] +visible = false +offset = Vector2(0, -20) +scale = Vector2(1, 1) + [node name="Label" type="Label" parent="."] offset_right = 64.0 offset_bottom = 24.0 -theme_override_font_sizes/font_size = 18 +theme = SubResource("Theme_floating_text") text = "+1 coin" horizontal_alignment = 1 vertical_alignment = 1 diff --git a/src/scenes/game_world.tscn b/src/scenes/game_world.tscn index 653ec66..5d30711 100644 --- a/src/scenes/game_world.tscn +++ b/src/scenes/game_world.tscn @@ -2,7 +2,6 @@ [ext_resource type="Script" uid="uid://bax7e73v836nx" path="res://scripts/player_manager.gd" id="1"] [ext_resource type="PackedScene" uid="uid://cxfvw8y7jqn2p" path="res://scenes/player.tscn" id="2"] -[ext_resource type="PackedScene" uid="uid://b7qx8y2jqn3r" path="res://scenes/interactable_object.tscn" id="3"] [ext_resource type="Script" uid="uid://db58xcyo4cjk" path="res://scripts/game_world.gd" id="4"] [ext_resource type="Script" uid="uid://wff5063ctp7g" path="res://scripts/debug_overlay.gd" id="5"] [ext_resource type="TileSet" uid="uid://dqem5tbvooxrg" path="res://assets/gfx/RPG DUNGEON VOL 3.tres" id="9"] @@ -28,46 +27,9 @@ modulate = Color(1, 1, 1, 0.77254903) z_index = 1 tile_set = ExtResource("9") -[node name="Floor" type="Polygon2D" parent="Environment" unique_id=1715441485] -visible = false -color = Color(0.3, 0.3, 0.3, 1) -polygon = PackedVector2Array(-1000, -1000, 1000, -1000, 1000, 1000, -1000, 1000) -metadata/_edit_lock_ = true - -[node name="Walls" type="StaticBody2D" parent="Environment" unique_id=336033150] -collision_layer = 4 -collision_mask = 3 - -[node name="WallTop" type="CollisionPolygon2D" parent="Environment/Walls" unique_id=1311846641] -polygon = PackedVector2Array(-1020, -1020, 1020, -1020, 1020, -980, -1020, -980) - -[node name="WallBottom" type="CollisionPolygon2D" parent="Environment/Walls" unique_id=902776066] -polygon = PackedVector2Array(-1020, 980, 1020, 980, 1020, 1020, -1020, 1020) - -[node name="WallLeft" type="CollisionPolygon2D" parent="Environment/Walls" unique_id=1762713816] -polygon = PackedVector2Array(-1020, -980, -980, -980, -980, 980, -1020, 980) - -[node name="WallRight" type="CollisionPolygon2D" parent="Environment/Walls" unique_id=540990153] -polygon = PackedVector2Array(980, -980, 1020, -980, 1020, 980, 980, 980) - [node name="Entities" type="Node2D" parent="." unique_id=1447395523] y_sort_enabled = true -[node name="Box1" parent="Entities" unique_id=2016646819 instance=ExtResource("3")] -position = Vector2(101, 66) - -[node name="Box2" parent="Entities" unique_id=219568153 instance=ExtResource("3")] -position = Vector2(100, 133) - -[node name="Box3" parent="Entities" unique_id=1831798906 instance=ExtResource("3")] -position = Vector2(113, 9) - -[node name="Box4" parent="Entities" unique_id=140447274 instance=ExtResource("3")] -position = Vector2(198, 58) - -[node name="Box5" parent="Entities" unique_id=284709248 instance=ExtResource("3")] -position = Vector2(74, 12) - [node name="DebugOverlay" type="CanvasLayer" parent="." unique_id=1325005956] script = ExtResource("5") diff --git a/src/scenes/smoke_puff.tscn b/src/scenes/smoke_puff.tscn index 3d3b5ea..becb2fc 100644 --- a/src/scenes/smoke_puff.tscn +++ b/src/scenes/smoke_puff.tscn @@ -1,14 +1,12 @@ -[gd_scene load_steps=3 format=3 uid="uid://bqvx8y2jqn4s"] +[gd_scene format=3 uid="uid://bqvx8y2jqn4s"] -[ext_resource type="Script" path="res://scripts/smoke_puff.gd" id="1_puff"] +[ext_resource type="Script" uid="uid://px6532483e6t" path="res://scripts/smoke_puff.gd" id="1_puff"] [ext_resource type="Texture2D" uid="uid://bknascfv4twmi" path="res://assets/gfx/smoke_puffs.png" id="2_smoke"] -[node name="SmokePuff" type="Node2D"] +[node name="SmokePuff" type="Node2D" unique_id=243995580] script = ExtResource("1_puff") -[node name="Sprite2D" type="Sprite2D" parent="."] +[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1282738570] texture = ExtResource("2_smoke") hframes = 4 vframes = 2 -frame = 0 - diff --git a/src/scripts/damage_number.gd b/src/scripts/damage_number.gd index 19d5720..370334b 100644 --- a/src/scripts/damage_number.gd +++ b/src/scripts/damage_number.gd @@ -2,11 +2,12 @@ extends Label @export var label: String = "1" -@export var color := Color(1, 1, 1, 1) -@export var direction := Vector2.ZERO # Default direction -var fade_delay := 0.6 # When to start fading (mid-move) -var move_duration := 0.8 # Slash exists for 0.3 seconds -var fade_duration := 0.2 # Time to fade out +@export var color := Color.RED # Red color for damage numbers +@export var direction := Vector2.ZERO # Default direction (will be random if not set) +var fade_delay := 0.6 # When to start fading (display duration) +var move_duration := 1.0 # Total animation duration (includes fade) +var fade_duration := 0.4 # Time to fade out (after fade_delay) +var rise_distance: float = 20.0 # Distance to move upward var stretch_amount := Vector2(1, 1.4) # How much to stretch the sprite # Called when the node enters the scene tree for the first time. @@ -15,20 +16,35 @@ func _ready() -> void: pass # Replace with function body. func _initialize_damage_number() -> void: + # Set color (red by default) and text self.modulate = color self.text = label - var tween = create_tween() - var move_target = global_position + (direction.normalized() * 10) # Moves in given direction - tween.set_trans(Tween.TRANS_CUBIC) # Smooth acceleration & deceleration - tween.set_ease(Tween.EASE_OUT) # Fast start, then slows down - tween.tween_property(self, "global_position", move_target, move_duration) - - # Wait until mid-move to start fade + + # If direction is not set, use a random upward direction with slight variation + if direction == Vector2.ZERO: + var random_angle = randf_range(-PI/6, PI/6) # ±30 degrees from straight up + direction = Vector2(sin(random_angle), -cos(random_angle)) # Mostly upward with slight variation + + # Calculate target position (move upward with slight horizontal variation) + var move_target = global_position + (direction.normalized() * rise_distance) + + # Total animation duration = display (0.6s) + fade (0.4s) = 1.0s + var total_duration = fade_delay + fade_duration # 0.6 + 0.4 = 1.0s + + # Create tween for movement (entire duration, continues during fade) + var move_tween = create_tween() + move_tween.set_trans(Tween.TRANS_CUBIC) + move_tween.set_ease(Tween.EASE_OUT) + move_tween.tween_property(self, "global_position", move_target, total_duration) + + # Wait for display duration (0.6s), then start fading await get_tree().create_timer(fade_delay).timeout - - # Start fade-out effect + + # Fade out over fade_duration (0.4s) while still moving var fade_tween = create_tween() - fade_tween.tween_property(self, "modulate:a", 0.0, fade_duration) # Fade to transparent + fade_tween.tween_property(self, "modulate:a", 0.0, fade_duration) + + # Wait for fade to complete, then remove await fade_tween.finished queue_free() pass diff --git a/src/scripts/door.gd b/src/scripts/door.gd index 70208da..e79f533 100644 --- a/src/scripts/door.gd +++ b/src/scripts/door.gd @@ -11,11 +11,36 @@ var is_closing:bool = false var is_opening:bool = false var time_to_move:float = 0.5 var move_timer:float = 0.0 +var animation_start_position: Vector2 = Vector2.ZERO # Position when animation started -var initial_position:Vector2 = Vector2.ZERO +var closed_position: Vector2 = Vector2.ZERO # Position when door is closed (local) +var open_offset: Vector2 = Vector2.ZERO # Offset from closed to open position (local) + +# Room and puzzle state +var blocking_room: Dictionary = {} # Room this door blocks (the room you enter INTO) +var room1: Dictionary = {} # First room connected by this door (room you leave FROM) +var room2: Dictionary = {} # Second room connected by this door (room you enter INTO - the blocking_room) +var switch_room: Dictionary = {} # Room where the switch is located (before the door) +var room_trigger_area: Area2D = null # Reference to room trigger area for the blocking room +var puzzle_solved: bool = false # True when room puzzle is solved +var enemies_defeated: bool = false # True when all enemies in room are defeated +var switches_activated: bool = false # True when required switches are activated + +# Key door state +var key_used: bool = false # True when key has been used +var key_indicator: Sprite2D = null # Visual indicator showing key above door + +# Floor switches this door is connected to +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 # Called when the node enters the scene tree for the first time. func _ready() -> void: + # Set texture based on door type + _update_door_texture() + + # Rotate door first based on direction (original order) if direction == "Left": self.rotate(-PI/2) elif direction == "Right": @@ -23,66 +48,762 @@ func _ready() -> void: elif direction == "Down": self.rotate(PI) - initial_position = global_position - var amount = 16 - set_collision_layer_value(7, false) - if is_closed: - set_collision_layer_value(7, true) - amount = 0 + # Calculate open offset based on direction (in WORLD space) + # NEW RULES: + # - Open state: door is at specific tile (UP:tile2, RIGHT:tile4, DOWN:tile2, LEFT:tile3) + # - Closed state: door moves 16 pixels offset from open position + # - UP: closed = open + (0, 16) = 16px down (from tile 2 to tile 5) + # - RIGHT: closed = open + (-16, 0) = 16px left (from tile 4 to tile 3) + # - DOWN: closed = open + (0, 16) = 16px down (from tile 2 to tile 5) + # - LEFT: closed = open + (16, 0) = 16px right (from tile 3 to tile 4) + var open_amount = 16.0 + open_offset = Vector2.ZERO + $TeleporterIntoClosedRoom.is_enabled = false # disable initially (only enable on closed!) + if direction == "Up": - position.y = initial_position.y - amount + # Door on top wall: closed state is 16px DOWN from open state + # So open_offset is positive Y (door moves down when closing, so open is up) + # Actually wait - if closed is 16px down from open, then open is 16px up from closed + # So open_offset should be negative Y (open position is above closed position) + open_offset = Vector2(0, -open_amount) # Open is 16px UP from closed elif direction == "Down": - position.y = initial_position.y + amount + # Door on bottom wall: + # For StoneDoor/GateDoor: open is at (col 1, row 1), closed is at (col 1, row 0) + # So closed is 16px UP from open, open_offset = (0, -16) means open is 16px DOWN from closed + # For KeyDoor: closed is at (col 1, row 0), open is at (col 1, row 1) + # So open is 16px DOWN from closed, open_offset = (0, 16) + # NOTE: This is recalculated in _ready_after_setup() based on door type + open_offset = Vector2(0, -open_amount) # For StoneDoor/GateDoor: open is 16px DOWN from closed elif direction == "Left": - position.x = initial_position.x - amount + # Door on left wall: closed state is 16px RIGHT from open state + # So open_offset is positive X (door moves right when closing, so open is left) + # Actually wait - if closed is 16px right from open, then open is 16px left from closed + # So open_offset should be negative X (open position is left of closed position) + open_offset = Vector2(-open_amount, 0) # Open is 16px LEFT from closed elif direction == "Right": - position.x = initial_position.x + amount + # Door on right wall: closed state is 16px LEFT from open state + # So open_offset is negative X (door moves left when closing, so open is right) + # Actually wait - if closed is 16px left from open, then open is 16px right from closed + # So open_offset should be positive X (open position is right of closed position) + open_offset = Vector2(open_amount, 0) # Open is 16px RIGHT from closed + # Note: closed_position will be set in _ready_after_setup after door is positioned + # For now, just initialize it + closed_position = position - - pass # Replace with function body. + # Connect KeyInteractionArea signal + var key_area = get_node_or_null("KeyInteractionArea") + if key_area: + key_area.body_entered.connect(_on_key_interaction_area_body_entered) + + # Call setup after a frame to ensure everything is ready + call_deferred("_ready_after_setup") +func _update_door_texture(): + # Update door texture based on door type + var sprite = get_node_or_null("Sprite2D") + if not sprite: + return + + match type: + "KeyDoor": + var locked_texture = load("res://assets/gfx/door_locked.png") + if locked_texture: + sprite.texture = locked_texture + print("Door: Set KeyDoor texture to door_locked.png") + else: + push_error("Door: Could not load door_locked.png texture!") + "GateDoor": + var gate_texture = load("res://assets/gfx/door_gate.png") + if gate_texture: + sprite.texture = gate_texture + print("Door: Set GateDoor texture to door_gate.png") + else: + push_error("Door: Could not load door_gate.png texture!") + "StoneDoor": + # Use door_barred.png for stone doors + var barred_texture = load("res://assets/gfx/door_barred.png") + if barred_texture: + sprite.texture = barred_texture + print("Door: Set StoneDoor texture to door_barred.png") + else: + push_error("Door: Could not load door_barred.png texture!") # Called every frame. 'delta' is the elapsed time since the previous frame. func _process(delta: float) -> void: - # TODO write code to open/close door here - # when door is open, ofcourse + # Handle door opening/closing animation if is_opening or is_closing: - move_timer+=delta - #move 16 pixels in direction under 0.5 seconds - var amount = clamp(16*(move_timer/time_to_move),0,16) - if is_closing: - amount = 16-amount - if direction == "Up": - position.y = initial_position.y - amount - elif direction == "Down": - position.y = initial_position.y + amount - elif direction == "Left": - position.x = initial_position.x - amount - elif direction == "Right": - position.x = initial_position.x + amount - if move_timer >= time_to_move: - if is_opening: - is_closed = false - set_collision_layer_value(7, false) - else: - is_closed = true - set_collision_layer_value(7, true) + # Safety check: ensure closed_position is valid before animating + if closed_position == Vector2.ZERO: + print("Door: ERROR - closed_position is zero during animation! Resetting...") + closed_position = position - open_offset if is_opening else position is_opening = false is_closing = false - move_timer = 0 - pass + move_timer = 0.0 + # Only update collision for StoneDoor and GateDoor (KeyDoors handle their own state) + if type == "StoneDoor" or type == "GateDoor": + _update_collision_based_on_position() + return + + move_timer += delta + var progress = clamp(move_timer / time_to_move, 0.0, 1.0) + + if is_opening: + # Interpolate from closed to open position + # Start at closed_position (or animation_start_position if set), end at closed_position + open_offset (moving AWAY from closed position) + var start_pos = animation_start_position if animation_start_position != Vector2.ZERO else closed_position + var target_pos = closed_position + open_offset + position = start_pos.lerp(target_pos, progress) + global_position = position # Also update global position during animation + # Debug: log for KeyDoors to verify movement + if type == "KeyDoor" and move_timer < 0.1: # Only log once at start of animation + print("Door: KeyDoor opening animation - start: ", start_pos, ", target: ", target_pos, ", offset: ", open_offset, ", direction: ", direction) + + # For KeyDoors: disable collision as soon as opening starts (allow passage immediately) + # For StoneDoor/GateDoor: update collision based on position + if type == "KeyDoor": + # KeyDoors: disable collision immediately when opening starts + if get_collision_layer_value(7): + set_collision_layer_value(7, false) + elif type == "StoneDoor" or type == "GateDoor": + # Update collision based on distance to closed position (disable when moving away) + var dist_to_closed = position.distance_to(closed_position) + if dist_to_closed > 5.0: + # Moving away from closed position - disable collision + if get_collision_layer_value(7): + set_collision_layer_value(7, false) + else: + # Still near closed position - keep collision enabled + if not get_collision_layer_value(7): + set_collision_layer_value(7, true) + elif is_closing: + # Interpolate from open to closed position + # NOTE: KeyDoors should NEVER close (only open with key) + # CRITICAL: Use stored starting position (set when animation started in _close()) + # If animation_start_position wasn't set, calculate open position from closed_position + open_offset + var start_pos = animation_start_position if animation_start_position != Vector2.ZERO else (closed_position + open_offset) + position = start_pos.lerp(closed_position, progress) + global_position = position # Also update global position during animation + + # Update collision for StoneDoor/GateDoor only + if type == "StoneDoor" or type == "GateDoor": + # Update collision based on distance to closed position (enable when approaching closed) + var dist_to_closed = position.distance_to(closed_position) + if dist_to_closed <= 5.0: + # At or near closed position - enable collision + if not get_collision_layer_value(7): + set_collision_layer_value(7, true) + else: + # Still away from closed position - keep collision disabled + if get_collision_layer_value(7): + set_collision_layer_value(7, false) + + if move_timer >= time_to_move: + # Animation complete + if is_opening: + is_closed = false + # Move door to open position (away from closed position) + var open_position = closed_position + open_offset + position = open_position + global_position = open_position # Also set global position + # 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") + # Animation finished, reset flags + is_opening = false + is_closing = false + move_timer = 0.0 + animation_start_position = Vector2.ZERO # Reset animation start position + else: + # Closing animation complete + is_closed = true + position = closed_position + global_position = closed_position # Also set global position + # 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") + # Animation finished, reset flags + is_opening = false + is_closing = false + move_timer = 0.0 + animation_start_position = Vector2.ZERO # Reset animation start position + # Now that door has finished closing, check if puzzle is solved (to open it immediately if already solved) + if type == "StoneDoor" or type == "GateDoor": + _check_puzzle_state() + + # Update collision based on actual position (safety check in case position was changed externally) + # CRITICAL: KeyDoors should NEVER have their position/state changed automatically! + # Only update if not currently animating (to avoid interfering with animation) + # Only update for StoneDoor and GateDoor (NOT KeyDoors) + # IMPORTANT: Only update collision, don't change position - that could interfere with initial setup + if not is_opening and not is_closing and (type == "StoneDoor" or type == "GateDoor"): + # Only update collision based on position, don't change position or is_closed flag + # Position and is_closed should only be changed by explicit _open()/_close() calls or animation + var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0 + if distance_to_closed <= 1.0: + # At closed position - collision should be ENABLED + if not get_collision_layer_value(7): + set_collision_layer_value(7, true) + else: + # Away from closed position (open) - collision should be DISABLED + if get_collision_layer_value(7): + set_collision_layer_value(7, false) + + # For StoneDoor and GateDoor, periodically check puzzle state (only if door is closed and puzzle not solved) + # CRITICAL: Only check puzzle state if door has puzzle elements (switches or enemies) + # If door has no puzzle elements, it should never open + # Check every 10 frames (0.16 seconds at 60fps) to reduce performance impact + var check_puzzle_timer = Engine.get_process_frames() % 10 + if check_puzzle_timer == 0 and (type == "StoneDoor" or type == "GateDoor") and is_closed and not puzzle_solved: + # Check if door requires enemies or switches + if requires_enemies or requires_switch: + _check_puzzle_state() + + # For KeyDoors, ensure they stay at closed position if not opened + # KeyDoors should NEVER move unless explicitly opened with a key + if type == "KeyDoor" and not is_opening and not is_closing and not key_used: + # Ensure KeyDoor is at closed position and has collision enabled + if closed_position != Vector2.ZERO: + # Snap to closed position if somehow moved (shouldn't happen, but safety check) + var distance_to_closed = position.distance_to(closed_position) + if distance_to_closed > 1.0: + print("Door: KeyDoor was moved incorrectly! Resetting to closed position.") + position = closed_position + is_closed = true + set_collision_layer_value(7, true) + +func _update_collision_based_on_position(): + # Update collision based on whether door is at closed position or moved away + # CRITICAL: This function should NEVER be called for KeyDoors! + # Only for StoneDoor and GateDoor + # CRITICAL: This function ONLY updates collision, it does NOT change position or is_closed flag + # Position and is_closed should only be changed by explicit _open()/_close() calls or animation + if type == "KeyDoor": + return # Don't update KeyDoors - they handle their own state + + # Only update collision, don't change position or is_closed flag + var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0 + var distance_threshold = 1.0 # Consider "at closed position" if within 1 pixel + + if distance_to_closed <= distance_threshold: + # Door is at closed position - collision should be ENABLED + if not get_collision_layer_value(7): + set_collision_layer_value(7, true) + else: + # Door is moved away from closed position (open) - collision should be DISABLED + if get_collision_layer_value(7): + set_collision_layer_value(7, false) func _open(): - $SfxOpenKeyDoor.play() + $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) + if type == "KeyDoor": + # KeyDoors should always be at closed position when opening starts + # If somehow moved, reset to closed position first + if closed_position != Vector2.ZERO: + # Reset to closed position to ensure animation starts from correct position + position = closed_position + global_position = closed_position + is_closed = true + set_collision_layer_value(7, true) # Collision enabled at closed position + print("Door: KeyDoor _open() called - reset to closed position ", closed_position, " before opening") + else: + push_error("Door: KeyDoor _open() called but closed_position is zero!") + return + $SfxOpenKeyDoor.play() + else: + # StoneDoor/GateDoor: Only open if door is currently closed + 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 is_actually_open: + # Door is already open - don't do anything + print("Door: _open() called but door is already open! Position: ", position, ", closed: ", closed_position, ", distance: ", distance_to_closed) + # Ensure door is at open position and collision is disabled + var open_pos = closed_position + open_offset + position = open_pos + global_position = open_pos + is_closed = false + set_collision_layer_value(7, false) + return # Don't start animation + + # Door is closed - ensure it's at closed position before opening + if closed_position != Vector2.ZERO: + position = closed_position + global_position = closed_position + is_closed = true + set_collision_layer_value(7, true) + print("Door: StoneDoor/GateDoor _open() called - ensuring door is at closed position ", closed_position, " before opening") + else: + push_error("Door: StoneDoor/GateDoor _open() called but closed_position is zero!") + return + + $SfxOpenStoneDoor.play() + + # CRITICAL: Store starting position for animation (should be closed_position) + animation_start_position = position + print("Door: Starting open animation from ", animation_start_position, " to ", closed_position + open_offset, " (offset: ", open_offset, ")") is_opening = true is_closing = false move_timer = 0.0 - pass func _close(): - $SfxOpenStoneDoor.play() + # 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!") + return + + # Ensure closed_position is valid before closing + if closed_position == Vector2.ZERO: + # If closed_position wasn't set correctly, use current position + closed_position = position + print("Door: WARNING - closed_position was zero, using current position: ", closed_position) + + # Check both flag and actual position to determine door state + 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 # Within 5 pixels of closed position + + print("Door: _close() called - is_closed: ", is_closed, ", is_actually_at_closed: ", is_actually_at_closed, ", position: ", position, ", closed: ", closed_position, ", distance: ", distance_to_closed) + + # If door is already at closed position (both visually and by flag), don't do anything + if is_closed and is_actually_at_closed and not is_opening and not is_closing: + print("Door: Already closed (both flag and position match), not closing again") + return # Already closed, don't do anything + + # CRITICAL: If door is at closed position but flag says open, just fix the state - don't animate + if is_actually_at_closed and not is_closed: + # Door is visually at closed position but flag says open - fix state only + print("Door: Door is at closed position but flag says open! Fixing state only (no animation)") + position = closed_position # Ensure exact position + is_closed = true + set_collision_layer_value(7, true) + return # Don't start animation + + # Door is actually open (position is away from closed position) - start closing animation + # CRITICAL: Store starting position BEFORE starting animation + # Calculate expected open position (closed_position + open_offset) + var expected_open_pos = closed_position + open_offset + var distance_to_open = position.distance_to(expected_open_pos) + + # Use current position as start (it should already be at open position) + # If door is significantly away from expected open position, snap to open position first + if distance_to_open > 10.0: + # Door is very far from expected open position - reset to open position first + print("Door: WARNING - Door is far from expected open position! Resetting to open: ", expected_open_pos, " (was at: ", position, ", distance: ", distance_to_open, ")") + animation_start_position = expected_open_pos + position = expected_open_pos + global_position = expected_open_pos + is_closed = false + set_collision_layer_value(7, false) + else: + # Door is at or near open position - use current position as start + animation_start_position = position + + print("Door: Starting close animation from ", animation_start_position, " to ", closed_position, " (offset: ", open_offset, ")") + $SfxDoorCloses.play() is_opening = false is_closing = true move_timer = 0.0 + $TeleporterIntoClosedRoom.is_enabled = true + +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 + # The position set by game_world is the OPEN position (initial state for blocking doors) + var open_position = position # Current position is the OPEN position (from tile coordinates) + + print("Door: _ready_after_setup() called - type: ", type, ", direction: ", direction, ", is_closed: ", is_closed, ", open_position: ", open_position) + + # CRITICAL: Calculate closed position based on direction + # For StoneDoor/GateDoor: They start OPEN, then CLOSE when entering room + # For KeyDoor: They start CLOSED, then OPEN when key is used + # - UP: closed = open + (0, 16) = 16px down (from tile 2 to tile 5) + # - RIGHT: closed = open + (-16, 0) = 16px left (from tile 4 to tile 3) + # - DOWN: For StoneDoor/GateDoor: closed = open + (0, -16) = 16px UP (from row 1 to row 0) + # For KeyDoor: open = closed + (0, 16) = 16px DOWN (from row 0 to row 1) + # - LEFT: closed = open + (16, 0) = 16px right (from tile 3 to tile 4) + var closed_offset = Vector2.ZERO + match direction: + "Up": + closed_offset = Vector2(0, 16) # Closed is 16px DOWN from open + "Down": + # CRITICAL: For StoneDoor/GateDoor, they start OPEN at (col 1, row 1) and close to (col 1, row 0) + # So closed is 16px UP from open (negative Y) + # For KeyDoor, they start CLOSED at (col 1, row 0) and open to (col 1, row 1) + # But KeyDoor logic is handled separately in _ready_after_setup() + if type == "KeyDoor": + # KeyDoor: closed is at row 0, open is at row 1 (16px down) + # But we calculate from open_position, so this won't be used for KeyDoor + closed_offset = Vector2(0, -16) # Won't be used - KeyDoor uses different logic + else: + # StoneDoor/GateDoor: open is at row 1, closed is at row 0 (16px up) + closed_offset = Vector2(0, -16) # Closed is 16px UP from open + "Left": + closed_offset = Vector2(16, 0) # Closed is 16px RIGHT from open + "Right": + closed_offset = Vector2(-16, 0) # Closed is 16px LEFT from open + + closed_position = open_position + closed_offset + + # Update open_offset for animation logic (offset from closed to open) + # This is used when opening from closed position + open_offset = -closed_offset # open_offset = (0, -16) means open is 16px up from closed + + print("Door: Calculated positions - open: ", open_position, ", closed: ", closed_position, ", closed_offset: ", closed_offset, ", open_offset: ", open_offset) + + # CRITICAL: KeyDoors should ALWAYS start closed, regardless of is_closed value + # KeyDoors should NEVER be moved until opened with a key + # For KeyDoors: game_world positions them at CLOSED position (row 0 for Down doors) + # When opened, they move to OPEN position (row 1 for Down doors) - 16px DOWN + if type == "KeyDoor": + # For KeyDoors, the position from game_world is the CLOSED position + # Calculate open position from closed position + var keydoor_closed_position = position # Current position is CLOSED (from game_world) + + # Calculate open position based on direction + var keydoor_open_offset = Vector2.ZERO + match direction: + "Up": + keydoor_open_offset = Vector2(0, -16) # Open is 16px UP from closed + "Down": + keydoor_open_offset = Vector2(0, 16) # Open is 16px DOWN from closed (row 0 to row 1) + "Left": + 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 + + # Set positions correctly for KeyDoor + closed_position = keydoor_closed_position # Closed is where game_world placed it + open_offset = keydoor_open_offset # Offset to move from closed to open + + # KeyDoor starts CLOSED + is_closed = true + position = closed_position + global_position = closed_position + set_collision_layer_value(7, true) # Collision enabled when closed + print("Door: KeyDoor starting CLOSED at position ", position, " (direction: ", direction, "), will open to ", closed_position + open_offset, " - collision ENABLED") + # Create key indicator sprite for KeyDoor + _create_key_indicator() + return # Exit early for KeyDoors + elif is_closed: + # StoneDoor/GateDoor starting closed (shouldn't happen for blocking doors, but handle it) + position = closed_position + global_position = closed_position + is_closed = true # Ensure state matches position + set_collision_layer_value(7, true) + print("Door: Starting CLOSED at position ", position, " (type: ", type, ", direction: ", direction, ") - collision ENABLED") + else: + # StoneDoor/GateDoor starting OPEN (default for blocking doors) + # CRITICAL: Door MUST start at open position (which is where game_world placed it) + # Ensure position is EXACTLY at open_position (don't assume game_world set it correctly) + if position.distance_to(open_position) > 1.0: + # Position doesn't match open_position - force it to open position + print("Door: WARNING - Position doesn't match open_position! Forcing to open: ", open_position, " (was: ", position, ")") + position = open_position + + global_position = position # Ensure global_position matches position + is_closed = false # CRITICAL: State MUST be false (open) when at open position + set_collision_layer_value(7, false) # CRITICAL: Collision MUST be DISABLED when open + print("Door: Starting OPEN at position ", position, " (closed: ", closed_position, ", open: ", open_position, ", open_offset: ", open_offset, ", type: ", type, ", direction: ", direction, ") - collision DISABLED, is_closed: ", is_closed) + + # CRITICAL: Verify the door is actually at open position after setting it + var actual_distance = position.distance_to(closed_position) + var expected_distance = 16.0 # Should be 16 pixels away + if abs(actual_distance - expected_distance) > 2.0: + push_error("Door: ERROR - Door open/closed distance is wrong! Position: ", position, ", closed: ", closed_position, ", distance: ", actual_distance, " (expected: ", expected_distance, ")") + # Force it to correct open position + position = open_position + global_position = open_position + is_closed = false # CRITICAL: Ensure state is false when at open position + set_collision_layer_value(7, false) + print("Door: FORCED door to open position: ", position, " (distance to closed: ", position.distance_to(closed_position), ", is_closed: ", is_closed, ")") + + # FINAL VERIFICATION: Double-check state matches position + var distance_to_closed = position.distance_to(closed_position) + var should_be_open = distance_to_closed > 8.0 # If more than 8px from closed, should be open + if should_be_open and is_closed: + push_error("Door: ERROR - Door is at open position but is_closed is true! Fixing state...") + is_closed = false + set_collision_layer_value(7, false) + print("Door: Fixed state - door is now OPEN (is_closed: ", is_closed, ", collision: ", get_collision_layer_value(7), ")") + elif not should_be_open and not is_closed: + push_error("Door: ERROR - Door is at closed position but is_closed is false! Fixing state...") + is_closed = true + set_collision_layer_value(7, true) + print("Door: Fixed state - door is now CLOSED (is_closed: ", is_closed, ", collision: ", get_collision_layer_value(7), ")") + + # NOTE: Doors are NOT connected via signals to room triggers + # Instead, room triggers call door._on_room_entered() directly + # This prevents doors from reacting to ALL room entries, only their own blocking room + +func _create_key_indicator(): + # Create visual indicator for key above door + if key_indicator: + return # Already created + + key_indicator = Sprite2D.new() + # Load key texture from loot system + var key_texture = load("res://assets/gfx/pickups/items_n_shit.png") + if key_texture: + key_indicator.texture = key_texture + key_indicator.hframes = 20 + key_indicator.vframes = 14 + key_indicator.frame = (13 * 20) + 10 # Key frame from loot system + key_indicator.position = Vector2(0, -24) # Above door + key_indicator.visible = false # Hidden until key is used + add_child(key_indicator) + +func _on_room_entered(body): + # Player entered the room - close this door if puzzle not solved + # This door is IN the room that was just entered (room1 == entered room OR blocking_room == entered room) + if not body.is_in_group("player"): + return + + # Verify this door is in the room we just entered + if not room_trigger_area: + return # No trigger set, don't do anything + + var trigger_room = room_trigger_area.room if room_trigger_area.room else {} + var door_room1 = room1 if room1 else {} + var door_blocking_room = blocking_room if blocking_room else {} + + # Check if door is IN the trigger room (door starts FROM trigger room OR blocking_room == trigger room) + var door_in_trigger_room = false + if trigger_room and not trigger_room.is_empty(): + # Check room1 first (door starts FROM this room) + if door_room1 and not door_room1.is_empty(): + door_in_trigger_room = (door_room1.x == trigger_room.x and door_room1.y == trigger_room.y and \ + door_room1.w == trigger_room.w and door_room1.h == trigger_room.h) + + # Also check blocking_room (should match the puzzle room) + if not door_in_trigger_room and door_blocking_room and not door_blocking_room.is_empty(): + door_in_trigger_room = (door_blocking_room.x == trigger_room.x and door_blocking_room.y == trigger_room.y and \ + door_blocking_room.w == trigger_room.w and door_blocking_room.h == trigger_room.h) + + if not door_in_trigger_room: + # This door is NOT in the trigger room - ignore + return + + # This door is IN the room that was just entered - close it if puzzle not solved + if type == "StoneDoor" or type == "GateDoor": + # Close door if puzzle not solved and door is currently open + if not puzzle_solved: + # Check both is_closed flag AND actual position to determine door state + 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 door is more than 5 pixels away from closed_position, it's open + + print("Door: _on_room_entered() - type: ", type, ", is_closed: ", is_closed, ", is_actually_open: ", is_actually_open, ", position: ", position, ", closed: ", closed_position, ", distance: ", distance_to_closed) + + # CRITICAL: Only close if door is actually open (both flag and position must indicate open) + # If door is already closed, don't do anything + if is_actually_open and not is_closing and not is_opening: + # Door is actually open (position is away from closed position) - close it + print("Door: Closing door on room entry - was at position ", position, " (closed: ", closed_position, ", is_closed: ", is_closed, ", distance: ", distance_to_closed, ")") + + # Ensure door is at open position before closing + var expected_open_pos = closed_position + open_offset + var dist_to_open = position.distance_to(expected_open_pos) + if dist_to_open > 5.0: + # Door is not at expected open position - reset to open position first + print("Door: WARNING - Door is not at expected open position! Resetting to open: ", expected_open_pos, " (was at: ", position, ")") + position = expected_open_pos + global_position = expected_open_pos + is_closed = false + set_collision_layer_value(7, false) + + _close() + # Don't check puzzle state immediately - wait for door to finish closing + # Puzzle state will be checked when closing animation completes (in _process) + return # Exit early, don't check puzzle state yet + elif is_actually_open: + # Door is open but animation already in progress - don't interfere + print("Door: Door is open but animation in progress, not closing") + return + elif not is_actually_open: + # Door is already at closed position - but for StoneDoor/GateDoor, this shouldn't happen on room entry + # They should start OPEN and then CLOSE when entering room + # If door is at closed position, it might have been closed already - don't do anything + print("Door: WARNING - Door is already at closed position when entering room! This shouldn't happen for StoneDoor/GateDoor that start open.") + if closed_position != Vector2.ZERO: + # Ensure exact position and state match + position = closed_position + global_position = closed_position + is_closed = true + set_collision_layer_value(7, true) # Collision ENABLED when closed + print("Door: Door was already closed - ensuring state is correct, position: ", position, ", closed: ", closed_position) + # Now that door is confirmed closed, check if puzzle is already solved + # CRITICAL: Only check puzzle state if door is closed - don't check if puzzle is already solved + if not puzzle_solved: + _check_puzzle_state() + # If door is already closing (animation in progress), don't check puzzle state yet + # Puzzle state will be checked when closing animation completes (in _process) + +func _on_room_exited(body): + # Player left the room + if not body.is_in_group("player"): + return + # 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 + + # 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: + _open() + return + + # Check if all required switches are activated (switches in switch_room, before the door) + if _are_all_switches_activated(): + switches_activated = true + puzzle_solved = true + if is_closed: + _open() + return + +func _are_all_enemies_defeated() -> bool: + # Check if all enemies spawned from spawners in the puzzle room are defeated + # CRITICAL: Only check enemies that were SPAWNED from spawners (not pre-spawned enemies) + # Use room1 (the room this door is IN) or blocking_room for checking enemies + var target_room = room1 if room1 and not room1.is_empty() else blocking_room + if target_room.is_empty(): + return false + + # Find all enemies in the room that were spawned from spawners + var entities_node = get_tree().get_first_node_in_group("game_world") + if not entities_node: + entities_node = get_node("/root/GameWorld/Entities") + + if not entities_node: + return false + + var room_spawned_enemies = [] + for child in entities_node.get_children(): + if child.is_in_group("enemy"): + # CRITICAL: Only check enemies that were spawned from spawners (not pre-spawned) + if not child.has_meta("spawned_from_spawner") or not child.get_meta("spawned_from_spawner"): + continue # Skip pre-spawned enemies + + # 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 + + 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: + enemy_in_room = true + # Also check spawner metadata - if enemy has spawner_name matching this room's spawners + if child.has_meta("spawner_name"): + var spawner_name = child.get_meta("spawner_name") + # Spawner names are like "EnemySpawner__" + if str(target_room.x) in spawner_name and str(target_room.y) in spawner_name: + enemy_in_room = true # Confirmed by spawner name + + if enemy_in_room: + room_spawned_enemies.append(child) + print("Door: Found spawned enemy in room: ", child.name, " (spawner: ", child.get_meta("spawner_name") if child.has_meta("spawner_name") else "unknown", ", is_dead: ", child.is_dead if "is_dead" in child else "unknown", ")") + + # 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 "?", ")") + + if room_spawned_enemies.size() == 0: + # No spawned enemies found - if door requires enemies, puzzle is not solved + # But if there were never any enemies, this might mean they haven't spawned yet or all are already dead/removed + print("Door: No spawned enemies found in room - puzzle not solved yet (enemies may not have spawned or already removed)") + return false + + 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: + # Check if enemy is queued for deletion or removed from scene + 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 (is_dead: ", enemy_is_dead, ", is_queued: ", enemy.is_queued_for_deletion(), ", in_tree: ", enemy.is_inside_tree(), ")") + return false + else: + # Enemy is no longer valid (removed from scene) - consider it dead + print("Door: Enemy is no longer valid (removed from scene) - counting as dead") + + print("Door: All ", room_spawned_enemies.size(), " spawned enemies are dead! Puzzle solved!") + return true # All enemies are dead + +func _are_all_switches_activated() -> bool: + # Check if all required switches are activated + # CRITICAL: ONLY check connected_switches - switches are explicitly connected when spawned + # Do NOT use position-based fallback checks - they cause cross-room door triggering! + if connected_switches.size() > 0: + # Check all connected switches (these are the switches in THIS door's puzzle room) + print("Door: _are_all_switches_activated() - Checking ", connected_switches.size(), " connected switches for door ", name, " (room: ", blocking_room.get("x", "?"), ",", blocking_room.get("y", "?"), ")") + for switch in connected_switches: + if not is_instance_valid(switch): + continue + # is_activated is a variable, not a method + if not switch.is_activated: + print("Door: Switch ", switch.name, " is NOT activated") + return false + print("Door: All connected switches are activated!") + return true # All connected switches are activated + + # CRITICAL: If no switches are connected, the puzzle is NOT solved! + # Switches should ALWAYS be connected when spawned - if they're not, it's an error + print("Door: WARNING - Door ", name, " has no connected switches! Puzzle cannot be solved!") + return false # No connected switches means puzzle is NOT solved + +func _on_key_interaction_area_body_entered(body): + # Player entered key interaction area + if not body.is_in_group("player"): + return + + if type == "KeyDoor" and is_closed and not key_used: + # Check if player has a key + if body.has_method("has_key") and body.has_method("use_key"): + if body.has_key(): + # Use key and open door + body.use_key() + key_used = true + _show_key_indicator() + _open() + print("KeyDoor opened with key!") + +func _show_key_indicator(): + # Show key indicator above door + if key_indicator: + key_indicator.visible = true + # Make sure it's on top (higher z-index or add to front) + key_indicator.z_index = 10 + move_child(key_indicator, get_child_count() - 1) # Move to front + else: + # Create key indicator if it doesn't exist yet + _create_key_indicator() + if key_indicator: + key_indicator.visible = true + +func teleportPlayer(body: Node2D): + var keydoor_open_offset = Vector2.ZERO + match direction: + "Up": + keydoor_open_offset = Vector2(0, 16) # Open is 16px UP from closed + "Down": + keydoor_open_offset = Vector2(0, -16) # Open is 16px DOWN from closed (row 0 to row 1) + "Left": + 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 pass diff --git a/src/scripts/dungeon_generator.gd b/src/scripts/dungeon_generator.gd index ec20477..98aae91 100644 --- a/src/scripts/dungeon_generator.gd +++ b/src/scripts/dungeon_generator.gd @@ -95,7 +95,7 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) - # Calculate target room count based on level # Level 1: 7-8 rooms, then increase by 2-3 rooms per level - var target_room_count = 7 + (level - 1) * 2 + rng.randi_range(0, 1) # Level 1: 7-8, Level 2: 9-10, etc. + var target_room_count = 7 + (level - 1) * 2 + rng.randi_range(0, 1) # Level 1: 7-8, Level 2: 9-10, etc. print("DungeonGenerator: Level ", level, " - Target room count: ", target_room_count) # Initialize grid (0 = wall, 1 = floor, 2 = door, 3 = corridor) @@ -150,6 +150,7 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) - # 5. Mark start room (first room) var start_room_index = 0 + var exit_room_index = -1 # Declare exit_room_index early to avoid scope issues all_rooms[start_room_index].modifiers.append({"type": "START"}) # 6. Mark exit room (farthest REACHABLE room from start) @@ -157,33 +158,174 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) - var reachable_rooms = _find_reachable_rooms(all_rooms[start_room_index], all_rooms, all_doors) print("DungeonGenerator: Found ", reachable_rooms.size(), " reachable rooms from start (out of ", all_rooms.size(), " total)") - # Find the farthest reachable room - var exit_room_index = _find_farthest_room_from_list(all_rooms, start_room_index, reachable_rooms) - if exit_room_index == -1: - # Fallback: use the farthest room by distance (even if not reachable) - print("DungeonGenerator: WARNING - No reachable rooms found, using farthest by distance") + # CRITICAL: Remove inaccessible rooms (rooms not reachable from start) + # Store the start room before filtering (it should always be reachable) + var start_room_ref = all_rooms[start_room_index] + var inaccessible_count = 0 + + # Create new array with only reachable rooms + # Use value-based comparison (x, y, w, h) to check if room is reachable + var filtered_rooms = [] + for room in all_rooms: + var is_reachable = false + # Check if this room is in the reachable_rooms list by comparing values + for reachable_room in reachable_rooms: + if reachable_room.x == room.x and reachable_room.y == room.y and \ + reachable_room.w == room.w and reachable_room.h == room.h: + is_reachable = true + break + + if is_reachable: + filtered_rooms.append(room) + else: + inaccessible_count += 1 + print("DungeonGenerator: Removing inaccessible room at (", room.x, ", ", room.y, ") - no corridor connection") + + # Update all_rooms to only include reachable rooms + all_rooms = filtered_rooms + + if inaccessible_count > 0: + print("DungeonGenerator: Removed ", inaccessible_count, " inaccessible room(s). Remaining rooms: ", all_rooms.size()) + + # Update start_room_index after filtering (find start room in new array using value-based comparison) + start_room_index = -1 + for i in range(all_rooms.size()): + var room = all_rooms[i] + if room.x == start_room_ref.x and room.y == start_room_ref.y and \ + room.w == start_room_ref.w and room.h == start_room_ref.h: + start_room_index = i + break + + if start_room_index == -1: + push_error("DungeonGenerator: ERROR - Start room was removed! This should never happen!") + start_room_index = 0 # Fallback + + # Also remove doors connected to inaccessible rooms (clean up all_doors) + var filtered_doors = [] + var doors_removed = 0 + for door in all_doors: + var door_room1_reachable = false + var door_room2_reachable = false + + # Check if door's connected rooms are in the filtered reachable rooms list (all_rooms now only contains reachable rooms) + # Compare rooms by properties (x, y, w, h) since dictionary comparison might not work + # Check both room1 and room2 against all reachable rooms + if "room1" in door and door.room1 is Dictionary and not door.room1.is_empty(): + for room in all_rooms: + if door.room1.x == room.x and door.room1.y == room.y and \ + door.room1.w == room.w and door.room1.h == room.h: + door_room1_reachable = true + break # Found room1, no need to keep checking + + if "room2" in door and door.room2 is Dictionary and not door.room2.is_empty(): + for room in all_rooms: + if door.room2.x == room.x and door.room2.y == room.y and \ + door.room2.w == room.w and door.room2.h == room.h: + door_room2_reachable = true + break # Found room2, no need to keep checking + + # Only keep doors that connect two reachable rooms + if door_room1_reachable and door_room2_reachable: + filtered_doors.append(door) + else: + doors_removed += 1 + print("DungeonGenerator: Removing door - room1 reachable: ", door_room1_reachable, ", room2 reachable: ", door_room2_reachable) + + all_doors = filtered_doors + if doors_removed > 0: + print("DungeonGenerator: Removed ", doors_removed, " door(s) connected to inaccessible rooms. Remaining doors: ", all_doors.size()) + + # Find the farthest reachable room (now all rooms are reachable, but find farthest) + # Make sure we have at least 2 rooms (start and exit must be different) + # exit_room_index is already declared at function level + if all_rooms.size() < 2: + push_error("DungeonGenerator: ERROR - Not enough reachable rooms! Need at least 2 (start + exit), but only have ", all_rooms.size()) + # Use start room as exit if only one room exists (shouldn't happen, but handle gracefully) + if all_rooms.size() == 1: + exit_room_index = 0 + else: + # No rooms at all - this is a critical error + push_error("DungeonGenerator: CRITICAL ERROR - No rooms left after filtering!") + return {} # Return empty dungeon + else: exit_room_index = _find_farthest_room(all_rooms, start_room_index) + # Make sure exit room is different from start room + if exit_room_index == start_room_index and all_rooms.size() > 1: + # If exit is same as start, find second farthest + var max_distance = 0 + var second_farthest = -1 + for i in range(all_rooms.size()): + if i == start_room_index: + continue + var distance = abs(all_rooms[i].x - all_rooms[start_room_index].x) + abs(all_rooms[i].y - all_rooms[start_room_index].y) + if distance > max_distance: + max_distance = distance + second_farthest = i + if second_farthest != -1: + exit_room_index = second_farthest + all_rooms[exit_room_index].modifiers.append({"type": "EXIT"}) print("DungeonGenerator: Selected exit room at index ", exit_room_index, " position: ", all_rooms[exit_room_index].x, ",", all_rooms[exit_room_index].y) # 7. Render walls around rooms _render_room_walls(all_rooms, grid, tile_grid, map_size, rng) - # 8. Place torches in rooms + # 7.5. Place stairs in exit room BEFORE placing torches (so torches don't overlap stairs) + var stairs_data = _place_stairs_in_exit_room(all_rooms[exit_room_index], grid, tile_grid, map_size, all_doors, rng) + if stairs_data.is_empty(): + print("DungeonGenerator: ERROR - Failed to place stairs in exit room! Room size: ", all_rooms[exit_room_index].w, "x", all_rooms[exit_room_index].h, " Doors: ", all_doors.size()) + # CRITICAL: Force place stairs - we MUST have an exit! + print("DungeonGenerator: FORCING stairs placement in exit room center") + stairs_data = _force_place_stairs(all_rooms[exit_room_index], grid, tile_grid, map_size, all_doors, rng) + if stairs_data.is_empty(): + push_error("DungeonGenerator: CRITICAL ERROR - Could not place stairs even with force placement!") + + # 8. Place torches in rooms (AFTER stairs, so torches don't overlap stairs) var all_torches = [] for room in all_rooms: var room_torches = _place_torches_in_room(room, grid, all_doors, map_size, rng) all_torches.append_array(room_torches) - # 9. Place enemies in rooms (scaled by level, excluding start and exit rooms) + # 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) + + # Extract rooms with monster spawner puzzles (these should NOT have pre-spawned enemies) + var rooms_with_spawner_puzzles = [] + for door_data in blocking_doors: + 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 + # Check if this room is already in the list (avoid duplicates) + var already_in_list = false + for existing_room in rooms_with_spawner_puzzles: + if existing_room.x == puzzle_room.x and existing_room.y == puzzle_room.y and \ + existing_room.w == puzzle_room.w and existing_room.h == puzzle_room.h: + already_in_list = true + break + if not already_in_list: + rooms_with_spawner_puzzles.append(puzzle_room) + print("DungeonGenerator: Room (", puzzle_room.x, ", ", puzzle_room.y, ") has monster spawner puzzle - will skip pre-spawning enemies") + + # 9. Place enemies in rooms (scaled by level, excluding start and exit rooms, and rooms with spawner puzzles) var all_enemies = [] for i in range(all_rooms.size()): var room = all_rooms[i] # Skip start room and exit room if i != start_room_index and i != exit_room_index: - var room_enemies = _place_enemies_in_room(room, grid, map_size, rng, level) - all_enemies.append_array(room_enemies) + # CRITICAL: Skip rooms that have monster spawner puzzles (these will spawn enemies when player enters) + var has_spawner_puzzle = false + for spawner_room in rooms_with_spawner_puzzles: + if spawner_room.x == room.x and spawner_room.y == room.y and \ + spawner_room.w == room.w and spawner_room.h == room.h: + has_spawner_puzzle = true + print("DungeonGenerator: Skipping pre-spawned enemies for room (", room.x, ", ", room.y, ") - has monster spawner puzzle") + break + + if not has_spawner_puzzle: + var room_enemies = _place_enemies_in_room(room, grid, map_size, rng, level) + all_enemies.append_array(room_enemies) # 9.5. Place interactable objects in rooms (excluding start and exit rooms) var all_interactable_objects = [] @@ -194,15 +336,8 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) - var room_objects = _place_interactable_objects_in_room(room, grid, map_size, all_doors, all_enemies, rng) all_interactable_objects.append_array(room_objects) - # 10. Place stairs in exit room (make sure they don't overlap doors) - var stairs_data = _place_stairs_in_exit_room(all_rooms[exit_room_index], grid, tile_grid, map_size, all_doors, rng) - if stairs_data.is_empty(): - print("DungeonGenerator: ERROR - Failed to place stairs in exit room! Room size: ", all_rooms[exit_room_index].w, "x", all_rooms[exit_room_index].h, " Doors: ", all_doors.size()) - # CRITICAL: Force place stairs - we MUST have an exit! - print("DungeonGenerator: FORCING stairs placement in exit room center") - stairs_data = _force_place_stairs(all_rooms[exit_room_index], grid, tile_grid, map_size, all_doors, rng) - if stairs_data.is_empty(): - push_error("DungeonGenerator: CRITICAL ERROR - Could not place stairs even with force placement!") + # NOTE: Stairs placement was moved earlier (step 7.5, before torches) to prevent torch overlap + # NOTE: Blocking doors placement was moved earlier (step 11, before enemy placement) to identify spawner puzzle rooms return { "rooms": all_rooms, @@ -211,6 +346,7 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) - "enemies": all_enemies, "interactable_objects": all_interactable_objects, "stairs": stairs_data, + "blocking_doors": blocking_doors, "grid": grid, "tile_grid": tile_grid, "map_size": map_size, @@ -389,7 +525,7 @@ func _create_corridor_between_rooms(room1: Dictionary, room2: Dictionary, grid: var corridor_end_x = wall_x + corridor_length var corridor_y = door_y + 1 if corridor_intersects_other_room.call(corridor_start_x, corridor_y, corridor_end_x, corridor_y, true): - return {} # Corridor would pass through another room, skip this connection + return {} # Corridor would pass through another room, skip this connection # Create corridor (1 tile wide) - use floor tiles # Corridor is between the rooms, after the door @@ -429,14 +565,20 @@ func _create_corridor_between_rooms(room1: Dictionary, room2: Dictionary, grid: # Use door tile coordinates (5,2) + offset for 2x3 door tile_grid[x][y] = right_door_tile_start + Vector2i(door_dx, door_dy) + # CRITICAL: room1 = room the door is ON (left room for horizontal doors) + # room2 = room the door leads TO (right room for horizontal doors) + # For a door on the RIGHT wall of the left room, room1 = left_room, room2 = right_room + var door_room1 = left_room # Door is ON the left room's right wall + var door_room2 = right_room # Door leads TO the right room + return { "x": door_start_x, "y": door_y, "w": door_width, "h": 1, - "dir": "E" if left_room == room1 else "W", - "room1": room1, - "room2": room2 + "dir": "E", + "room1": door_room1, # CRITICAL: Door is IN the left room (on its right wall) + "room2": door_room2 # Door leads TO the right room } else: # Vertical corridor @@ -466,7 +608,7 @@ func _create_corridor_between_rooms(room1: Dictionary, room2: Dictionary, grid: var corridor_end_y = wall_y + corridor_length var corridor_x = door_x + 1 if corridor_intersects_other_room.call(corridor_x, corridor_start_y, corridor_x, corridor_end_y, false): - return {} # Corridor would pass through another room, skip this connection + return {} # Corridor would pass through another room, skip this connection # Create corridor (1 tile wide) - use floor tiles # Corridor is between the rooms, after the door @@ -506,14 +648,20 @@ func _create_corridor_between_rooms(room1: Dictionary, room2: Dictionary, grid: # Use door tile coordinates (7,0) + offset for 3x2 door tile_grid[x][y] = bottom_door_tile_start + Vector2i(door_dx, door_dy) + # CRITICAL: room1 = room the door is ON (top room for vertical doors) + # room2 = room the door leads TO (bottom room for vertical doors) + # For a door on the BOTTOM wall of the top room, room1 = top_room, room2 = bottom_room + var door_room1 = top_room # Door is ON the top room's bottom wall + var door_room2 = bottom_room # Door leads TO the bottom room + return { "x": door_x, "y": door_start_y, "w": 1, "h": door_height, - "dir": "S" if top_room == room1 else "N", - "room1": room1, - "room2": room2 + "dir": "S", + "room1": door_room1, # CRITICAL: Door is IN the top room (on its bottom wall) + "room2": door_room2 # Door leads TO the bottom room } func _find_closest_rooms(room: Dictionary, all_rooms: Array) -> Array: @@ -572,7 +720,7 @@ func _place_torches_in_room(room: Dictionary, grid: Array, all_doors: Array, _ma # Bottom wall center is at: (room.y + room.h - 1) * tile_size + tile_size / 2.0 # To get same distance from floor as top wall: move up 8 pixels from bottom wall center var bottom_wall_center_y = (room.y + room.h - 1) * tile_size + tile_size / 2.0 - var bottom_torch_y = bottom_wall_center_y - torch_y_offset # Move up 8 pixels from bottom wall center + var bottom_torch_y = bottom_wall_center_y - torch_y_offset # Move up 8 pixels from bottom wall center for x in range(room.x + 2, room.x + room.w - 2): # Check if this is a valid bottom wall position (check the lower part of bottom wall) var bottom_wall_y = room.y + room.h - 1 @@ -601,12 +749,12 @@ func _place_torches_in_room(room: Dictionary, grid: Array, all_doors: Array, _ma # Place at the same distance from floor as top wall torches # 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 + var left_wall_x = (room.x + 1) * tile_size + tile_size / 2.0 - 8 # Move 8 pixels to the left # Y position should be at the same distance from floor: y * tile_size + tile_size / 2.0 + 8 var left_wall_y = y * tile_size + tile_size / 2.0 + torch_y_offset var world_pos = Vector2(left_wall_x, left_wall_y) valid_wall_positions.append({"pos": world_pos, "wall": "left", "rotation": 270}) - break # Only add one torch per wall + break # Only add one torch per wall # Right wall (2 tiles wide: room.x + room.w - 2 and room.x + room.w - 1) # Place torches at the same distance from floor as top wall torches @@ -619,12 +767,12 @@ func _place_torches_in_room(room: Dictionary, grid: Array, all_doors: Array, _ma # Place at the same distance from floor as top wall torches # 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 + var right_wall_x = (room.x + room.w - 2) * tile_size + tile_size / 2.0 + 8 # Move 8 pixels to the right # Y position should be at the same distance from floor: y * tile_size + tile_size / 2.0 + 8 var right_wall_y = y * tile_size + tile_size / 2.0 + torch_y_offset var world_pos = Vector2(right_wall_x, right_wall_y) valid_wall_positions.append({"pos": world_pos, "wall": "right", "rotation": 90}) - break # Only add one torch per wall + break # Only add one torch per wall # Randomly select torch positions if valid_wall_positions.size() == 0: @@ -648,13 +796,16 @@ func _is_valid_torch_position(x: int, y: int, grid: Array, all_doors: Array) -> if x >= grid.size() or y >= grid[x].size(): return false - # Check if it's a door tile (grid_value == 2) - cannot place torch on door + # Check grid value - torches can only be placed on wall tiles (grid_value == 0) + # Cannot place on: doors (2), corridors (3), stairs (4), or floor (1) var grid_value = grid[x][y] - if grid_value == 2: - return false - - # Check if it's a wall tile (grid_value == 0) - this is valid for torches - if grid_value != 0: + if grid_value != 0: # Only wall tiles (0) are valid for torches + # Specifically check for stairs (4) to prevent overlap + if grid_value == 4: + return false # Stairs tile + if grid_value == 2: + return false # Door tile + # Any other non-wall value is invalid return false # Also check if position is within door area from door dictionaries @@ -662,8 +813,8 @@ 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_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) # Check if (x, y) is within door area # For horizontal doors: door.w is width (2 or 3), door.h is 1 @@ -677,18 +828,33 @@ func _find_reachable_rooms(start_room: Dictionary, _all_rooms: Array, all_doors: var reachable = [start_room] var queue = [start_room] + # Helper function to check if a room is already in the reachable list (value-based comparison) + var is_room_in_list = func(room_list: Array, room: Dictionary) -> bool: + if not room or room.is_empty(): + return false + for r in room_list: + if r.x == room.x and r.y == room.y and r.w == room.w and r.h == room.h: + return true + return false + while queue.size() > 0: var current = queue.pop_front() for door in all_doors: var next_room = null - if door.room1 == current: - next_room = door.room2 - elif door.room2 == current: - next_room = door.room1 + # Use value-based comparison for room matching + if "room1" in door and door.room1 is Dictionary: + if door.room1.x == current.x and door.room1.y == current.y and \ + door.room1.w == current.w and door.room1.h == current.h: + next_room = door.room2 if "room2" in door else null + if next_room == null and "room2" in door and door.room2 is Dictionary: + if door.room2.x == current.x and door.room2.y == current.y and \ + door.room2.w == current.w and door.room2.h == current.h: + next_room = door.room1 if "room1" in door else null - if next_room != null and not reachable.has(next_room): - reachable.append(next_room) - queue.append(next_room) + if next_room != null and next_room is Dictionary and not next_room.is_empty(): + if not is_room_in_list.call(reachable, next_room): + reachable.append(next_room) + queue.append(next_room) return reachable @@ -943,7 +1109,7 @@ func _place_enemies_in_room(room: Dictionary, grid: Array, map_size: Vector2i, r # Level 2: max 3 enemies per room # Level 3: max 4 enemies per room # Level 4+: max 5-6 enemies per room (scales with level) - var max_enemies = 2 if level == 1 else min(1 + level, 6) # Level 1: 2, Level 2: 3, Level 3: 4, Level 4: 5, Level 5+: 6 + var max_enemies = 2 if level == 1 else min(1 + level, 6) # Level 1: 2, Level 2: 3, Level 3: 4, Level 4: 5, Level 5+: 6 var num_enemies = rng.randi_range(0, max_enemies) # Available enemy types (scene paths) @@ -987,18 +1153,18 @@ func _place_enemies_in_room(room: Dictionary, grid: Array, map_size: Vector2i, r # Set appropriate move speed based on enemy type if enemy_type.ends_with("bat.tscn"): - move_speed = rng.randf_range(35.0, 45.0) # Bats: slower + move_speed = rng.randf_range(35.0, 45.0) # Bats: slower elif enemy_type.ends_with("slime.tscn"): - move_speed = rng.randf_range(30.0, 40.0) # Slimes: slowest + move_speed = rng.randf_range(18.0, 25.0) # Slimes: very slow (reduced from 30-40) elif enemy_type.ends_with("rat.tscn"): - move_speed = rng.randf_range(40.0, 50.0) # Rats: slow + move_speed = rng.randf_range(40.0, 50.0) # Rats: slow else: - move_speed = rng.randf_range(50.0, 80.0) # Other enemies (humanoids): faster + move_speed = rng.randf_range(50.0, 80.0) # Other enemies (humanoids): faster enemies.append({ "type": enemy_type, "position": position, - "room": room, # Store reference to room for AI + "room": room, # Store reference to room for AI "max_health": max_health, "move_speed": move_speed, "damage": damage @@ -1012,7 +1178,6 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A # Choose a random wall to place stairs on (excluding corners) # Make sure stairs don't overlap any doors # Returns stairs data with position and size for Area2D creation - print("DungeonGenerator: Placing stairs in exit room: ", exit_room.x, ",", exit_room.y, " size: ", exit_room.w, "x", exit_room.h, " doors: ", all_doors.size()) var stairs_data: Dictionary = {} @@ -1022,54 +1187,59 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A # Helper function to check if stairs position overlaps with any door # This checks if ANY tile of the stairs overlaps ANY tile of any door # Doors are either 3x2 (vertical: N/S) or 2x3 (horizontal: E/W) + # CRITICAL: Must check ALL door tiles, not just door position! + # Uses both door dictionary AND grid check for reliability var stairs_overlaps_door = func(stairs_x: int, stairs_y: int, stairs_w: int, stairs_h: int) -> bool: - for door in all_doors: - var door_x = door.x - var door_y = door.y - - # Determine actual door dimensions based on direction - # Horizontal doors (E/W): 2 tiles wide, 3 tiles tall - # Vertical doors (N/S): 3 tiles wide, 2 tiles tall - var door_w: int - var door_h: int - if "dir" in door: - match door.dir: - "E", "W": - # Horizontal doors: 2x3 - door_w = 2 - door_h = 3 - "N", "S": - # Vertical doors: 3x2 - door_w = 3 - door_h = 2 - _: - # Fallback: use stored values (shouldn't happen) - door_w = door.w if "w" in door else 3 - door_h = door.h if "h" in door else 2 - else: - # Fallback if no direction: assume vertical (3x2) - door_w = 3 - door_h = 2 - - # Check if stairs area overlaps door area (using strict overlap check) - # Two rectangles overlap if they share any common area - # They DON'T overlap if stairs is completely to the left, right, above, or below the door - var stairs_left = stairs_x - var stairs_right = stairs_x + stairs_w - var stairs_top = stairs_y - var stairs_bottom = stairs_y + stairs_h - - var door_left = door_x - var door_right = door_x + door_w - var door_top = door_y - var door_bottom = door_y + door_h - - # Check for overlap: rectangles overlap if they share any common area - # They overlap if NOT (stairs is completely left/right/above/below door) - if not (stairs_right <= door_left or stairs_left >= door_right or \ - stairs_bottom <= door_top or stairs_top >= door_bottom): - print("DungeonGenerator: Stairs at (", stairs_x, ",", stairs_y, ") size ", stairs_w, "x", stairs_h, " overlaps door at (", door_x, ",", door_y, ") size ", door_w, "x", door_h, " dir: ", door.dir if "dir" in door else "unknown") - return true + # FIRST: Check grid - if any stairs tile is marked as a door (grid value 2), it's an overlap + for stairs_tile_x in range(stairs_x, stairs_x + stairs_w): + for stairs_tile_y in range(stairs_y, stairs_y + stairs_h): + if stairs_tile_x >= 0 and stairs_tile_x < map_size.x and stairs_tile_y >= 0 and stairs_tile_y < map_size.y: + if grid[stairs_tile_x][stairs_tile_y] == 2: # Grid value 2 = door + print("DungeonGenerator: Stairs tile (", stairs_tile_x, ",", stairs_tile_y, ") is marked as door in grid!") + return true + + # SECOND: Check door dictionary - verify against all known doors + for stairs_tile_x in range(stairs_x, stairs_x + stairs_w): + for stairs_tile_y in range(stairs_y, stairs_y + stairs_h): + # Check this stairs tile against all door tiles from door dictionary + for door in all_doors: + var door_x = door.x + var door_y = door.y + + # Determine actual door dimensions and tile positions based on direction + # CRITICAL: Door x,y is the START position, but door spans multiple tiles! + var door_tiles: Array = [] # Array of {x, y} for each door tile + + if "dir" in door: + match door.dir: + "E", "W": + # Horizontal doors (E/W): 2x3 tiles (2 wide, 3 tall) + # Door starts at (door_x, door_y) and spans 2x3 + for door_dx in range(2): + for door_dy in range(3): + door_tiles.append({"x": door_x + door_dx, "y": door_y + door_dy}) + "N", "S": + # Vertical doors (N/S): 3x2 tiles (3 wide, 2 tall) + # Door starts at (door_x, door_y) and spans 3x2 + for door_dx in range(3): + for door_dy in range(2): + door_tiles.append({"x": door_x + door_dx, "y": door_y + door_dy}) + _: + # Fallback: assume 3x2 (vertical) + for door_dx in range(3): + for door_dy in range(2): + door_tiles.append({"x": door_x + door_dx, "y": door_y + door_dy}) + else: + # Fallback if no direction: assume 3x2 (vertical) + for door_dx in range(3): + for door_dy in range(2): + door_tiles.append({"x": door_x + door_dx, "y": door_y + door_dy}) + + # Check if this stairs tile matches any door tile + for door_tile in door_tiles: + if stairs_tile_x == door_tile.x and stairs_tile_y == door_tile.y: + print("DungeonGenerator: Stairs tile (", stairs_tile_x, ",", stairs_tile_y, ") overlaps door tile (", door_tile.x, ",", door_tile.y, ") from door at (", door_x, ",", door_y, ") dir: ", door.dir if "dir" in door else "unknown") + return true return false # Determine which walls are available (not blocked by doors) @@ -1078,31 +1248,34 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A # Top wall - stairs are 3 tiles wide, need at least 2 tiles from corners (same as doors) # Minimum room width: 2 (left buffer) + 3 (stairs) + 2 (right buffer) = 7 tiles - if exit_room.w >= 7: # Minimum room width for 3-tile stairs with corner buffers - var min_x = exit_room.x + 2 # At least 2 tiles from left corner - var max_x = exit_room.x + exit_room.w - 5 # At least 5 tiles from right edge (2 buffer + 3 stairs) + if exit_room.w >= 7: # Minimum room width for 3-tile stairs with corner buffers + var min_x = exit_room.x + 2 # At least 2 tiles from left corner + var max_x = exit_room.x + exit_room.w - 5 # At least 5 tiles from right edge (2 buffer + 3 stairs) if max_x >= min_x: wall_choices.append({ "dir": "UP", - "x_range": range(min_x, max_x + 1), # +1 because range is exclusive + "x_range": range(min_x, max_x + 1), # +1 because range is exclusive "y": exit_room.y, "tile_start": STAIRS_UP_START, "w": 3, "h": 2 }) - # Bottom wall - stairs are 3 tiles wide, need at least 2 tiles from corners + # Bottom wall - stairs are 3 tiles wide, need at least 2 tiles from corners (same as doors) # Minimum room width: 2 (left buffer) + 3 (stairs) + 2 (right buffer) = 7 tiles + # Use same logic as doors: at least 2 tiles from left corner, 5 tiles from right edge if exit_room.w >= 7: - var min_x = exit_room.x + 2 # At least 2 tiles from left corner - var max_x = exit_room.x + exit_room.w - 5 # At least 5 tiles from right edge (2 buffer + 3 stairs) + var min_x = exit_room.x + 2 # At least 2 tiles from left corner (same as doors) + var max_x = exit_room.x + exit_room.w - 5 # At least 5 tiles from right edge (2 buffer + 3 stairs) if max_x >= min_x: + # Bottom wall: door is at exit_room.y + exit_room.h - 2 (upper tile) and exit_room.y + exit_room.h - 1 (lower tile) + # Stairs should start at the same position as doors wall_choices.append({ "dir": "DOWN", "x_range": range(min_x, max_x + 1), - "y": exit_room.y + exit_room.h - 2, # Bottom wall is 2 tiles tall + "y": exit_room.y + exit_room.h - 2, # Start at upper wall tile (same as doors) "tile_start": STAIRS_DOWN_START, "w": 3, "h": 2 @@ -1110,9 +1283,9 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A # Left wall - stairs are 3 tiles tall, need at least 2 tiles from corners # Minimum room height: 2 (top buffer) + 3 (stairs) + 2 (bottom buffer) = 7 tiles - if exit_room.h >= 7: # Minimum room height for 3-tile stairs with corner buffers - var min_y = exit_room.y + 2 # At least 2 tiles from top corner - var max_y = exit_room.y + exit_room.h - 5 # At least 5 tiles from bottom edge (2 buffer + 3 stairs) + if exit_room.h >= 7: # Minimum room height for 3-tile stairs with corner buffers + var min_y = exit_room.y + 2 # At least 2 tiles from top corner + var max_y = exit_room.y + exit_room.h - 5 # At least 5 tiles from bottom edge (2 buffer + 3 stairs) if max_y >= min_y: wall_choices.append({ @@ -1127,14 +1300,14 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A # Right wall - stairs are 3 tiles tall, need at least 2 tiles from corners # Minimum room height: 2 (top buffer) + 3 (stairs) + 2 (bottom buffer) = 7 tiles if exit_room.h >= 7: - var min_y = exit_room.y + 2 # At least 2 tiles from top corner - var max_y = exit_room.y + exit_room.h - 5 # At least 5 tiles from bottom edge (2 buffer + 3 stairs) + var min_y = exit_room.y + 2 # At least 2 tiles from top corner + var max_y = exit_room.y + exit_room.h - 5 # At least 5 tiles from bottom edge (2 buffer + 3 stairs) if max_y >= min_y: wall_choices.append({ "dir": "RIGHT", "y_range": range(min_y, max_y + 1), - "x": exit_room.x + exit_room.w - 2, # Right wall is 2 tiles wide + "x": exit_room.x + exit_room.w - 2, # Right wall is 2 tiles wide "tile_start": STAIRS_RIGHT_START, "w": 2, "h": 3 @@ -1142,7 +1315,7 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A if wall_choices.size() == 0: print("DungeonGenerator: ERROR - No valid walls for stairs! Exit room too small: ", exit_room.w, "x", exit_room.h) - return {} # No valid walls for stairs + return {} # No valid walls for stairs # Choose a random wall var wall = wall_choices[rng.randi() % wall_choices.size()] @@ -1156,7 +1329,7 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A # Try to find a position that doesn't overlap doors var valid_positions = [] for test_x in wall.x_range: - var test_stairs_start_x = test_x - 1 # Start 1 tile to the left (3 tiles wide) + var test_stairs_start_x = test_x - 1 # Start 1 tile to the left (3 tiles wide) if not stairs_overlaps_door.call(test_stairs_start_x, wall.y, wall.w, wall.h): valid_positions.append(test_x) @@ -1164,11 +1337,11 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A print("DungeonGenerator: ERROR - No valid position found for ", wall.dir, " stairs (all positions overlap doors)") # Don't allow stairs to overlap doors - this is a critical bug # Instead, try the next wall or return empty to force placement elsewhere - return {} # No valid position found - will trigger _force_place_stairs + return {} # No valid position found - will trigger _force_place_stairs # Choose random valid position var stairs_x = valid_positions[rng.randi() % valid_positions.size()] - var stairs_start_x = stairs_x - 1 # Start 1 tile to the left (3 tiles wide) + var stairs_start_x = stairs_x - 1 # Start 1 tile to the left (3 tiles wide) # Store stairs data for Area2D creation stairs_data = { @@ -1189,23 +1362,24 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A var x = stairs_start_x + dx var y = wall.y + dy if x >= 0 and x < map_size.x and y >= 0 and y < map_size.y: - grid[x][y] = 4 # Stairs (use grid value 4) + grid[x][y] = 4 # Stairs (use grid value 4) # Render stairs tiles (similar to doors but with different middle frame) if wall.dir == "UP": - if dy == 0: # First row - if dx == 1: # Middle tile - tile_grid[x][y] = Vector2i(10, 0) # Special middle tile for UP stairs + if dy == 0: # First row + if dx == 1: # Middle tile + tile_grid[x][y] = Vector2i(10, 0) # Special middle tile for UP stairs else: tile_grid[x][y] = wall.tile_start + Vector2i(dx, dy) else: tile_grid[x][y] = wall.tile_start + Vector2i(dx, dy) - else: # DOWN - if dy == 1: # Second row - if dx == 1: # Middle tile - tile_grid[x][y] = Vector2i(6, 6) # Special middle tile for DOWN stairs - else: - tile_grid[x][y] = wall.tile_start + Vector2i(dx, dy) + else: # DOWN + # For DOWN stairs, use same tiles as door DOWN + # Row 0 (dy=0): use door tiles + # Row 1 (dy=1): use door tiles, except middle tile (col 1, row 1) which is 6,6 + if dy == 1 and dx == 1: # Second row, middle column (col 1, row 1) + tile_grid[x][y] = Vector2i(6, 6) # Special middle tile for DOWN stairs else: + # Use door DOWN tiles (same as DOOR_BOTTOM_START = 7,5) tile_grid[x][y] = wall.tile_start + Vector2i(dx, dy) elif wall.dir == "LEFT" or wall.dir == "RIGHT": @@ -1217,7 +1391,7 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A # Try to find a position that doesn't overlap doors var valid_positions = [] for test_y in wall.y_range: - var test_stairs_start_y = test_y - 1 # Start 1 tile up (3 tiles tall) + var test_stairs_start_y = test_y - 1 # Start 1 tile up (3 tiles tall) if not stairs_overlaps_door.call(wall.x, test_stairs_start_y, wall.w, wall.h): valid_positions.append(test_y) @@ -1225,11 +1399,11 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A print("DungeonGenerator: ERROR - No valid position found for ", wall.dir, " stairs (all positions overlap doors)") # Don't allow stairs to overlap doors - this is a critical bug # Instead, try the next wall or return empty to force placement elsewhere - return {} # No valid position found - will trigger _force_place_stairs + return {} # No valid position found - will trigger _force_place_stairs # Choose random valid position var stairs_y = valid_positions[rng.randi() % valid_positions.size()] - var stairs_start_y = stairs_y - 1 # Start 1 tile up (3 tiles tall) + var stairs_start_y = stairs_y - 1 # Start 1 tile up (3 tiles tall) # Store stairs data for Area2D creation stairs_data = { @@ -1250,20 +1424,20 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A var x = wall.x + dx var y = stairs_start_y + dy if x >= 0 and x < map_size.x and y >= 0 and y < map_size.y: - grid[x][y] = 4 # Stairs + grid[x][y] = 4 # Stairs # Render stairs tiles with special middle frame if wall.dir == "LEFT": - if dx == 0: # First column - if dy == 1: # Middle tile (second row) - tile_grid[x][y] = Vector2i(5, 1) # Special middle tile for LEFT stairs + if dx == 0: # First column + if dy == 1: # Middle tile (second row) + tile_grid[x][y] = Vector2i(5, 1) # Special middle tile for LEFT stairs else: tile_grid[x][y] = wall.tile_start + Vector2i(dx, dy) else: tile_grid[x][y] = wall.tile_start + Vector2i(dx, dy) - else: # RIGHT - if dx == 1: # Second column - if dy == 1: # Middle tile (second row) - tile_grid[x][y] = Vector2i(11, 1) # Special middle tile for RIGHT stairs + else: # RIGHT + if dx == 1: # Second column + if dy == 1: # Middle tile (second row) + tile_grid[x][y] = Vector2i(11, 1) # Special middle tile for RIGHT stairs else: tile_grid[x][y] = wall.tile_start + Vector2i(dx, dy) else: @@ -1281,42 +1455,54 @@ func _force_place_stairs(exit_room: Dictionary, grid: Array, tile_grid: Array, m var tile_size = 16 # Helper function to check if stairs position overlaps with any door (same as in _place_stairs_in_exit_room) - var stairs_overlaps_door = func(stairs_x: int, stairs_y: int, stairs_w: int, stairs_h: int) -> bool: - for door in all_doors: - var door_x = door.x - var door_y = door.y - - # Determine actual door dimensions based on direction - var door_w: int - var door_h: int - if "dir" in door: - match door.dir: - "E", "W": - door_w = 2 - door_h = 3 - "N", "S": - door_w = 3 - door_h = 2 - _: - door_w = door.w if "w" in door else 3 - door_h = door.h if "h" in door else 2 - else: - door_w = 3 - door_h = 2 - - var stairs_left = stairs_x - var stairs_right = stairs_x + stairs_w - var stairs_top = stairs_y - var stairs_bottom = stairs_y + stairs_h - - var door_left = door_x - var door_right = door_x + door_w - var door_top = door_y - var door_bottom = door_y + door_h - - if not (stairs_right <= door_left or stairs_left >= door_right or \ - stairs_bottom <= door_top or stairs_top >= door_bottom): - return true + # CRITICAL: Check each stairs tile against each door tile to ensure no overlap + # Uses both door dictionary AND grid check for reliability + var stairs_overlaps_door = func(test_x: int, test_y: int, test_w: int, test_h: int) -> bool: + # FIRST: Check grid - if any stairs tile is marked as a door (grid value 2), it's an overlap + for stairs_tile_x in range(test_x, test_x + test_w): + for stairs_tile_y in range(test_y, test_y + test_h): + if stairs_tile_x >= 0 and stairs_tile_x < map_size.x and stairs_tile_y >= 0 and stairs_tile_y < map_size.y: + if grid[stairs_tile_x][stairs_tile_y] == 2: # Grid value 2 = door + return true + + # SECOND: Check door dictionary - verify against all known doors + for stairs_tile_x in range(test_x, test_x + test_w): + for stairs_tile_y in range(test_y, test_y + test_h): + # Check this stairs tile against all door tiles from door dictionary + for door in all_doors: + var door_x = door.x + var door_y = door.y + + # Determine actual door tile positions based on direction + var door_tiles: Array = [] # Array of {x, y} for each door tile + + if "dir" in door: + match door.dir: + "E", "W": + # Horizontal doors (E/W): 2x3 tiles (2 wide, 3 tall) + for door_dx in range(2): + for door_dy in range(3): + door_tiles.append({"x": door_x + door_dx, "y": door_y + door_dy}) + "N", "S": + # Vertical doors (N/S): 3x2 tiles (3 wide, 2 tall) + for door_dx in range(3): + for door_dy in range(2): + door_tiles.append({"x": door_x + door_dx, "y": door_y + door_dy}) + _: + # Fallback: assume 3x2 (vertical) + for door_dx in range(3): + for door_dy in range(2): + door_tiles.append({"x": door_x + door_dx, "y": door_y + door_dy}) + else: + # Fallback: assume 3x2 (vertical) + for door_dx in range(3): + for door_dy in range(2): + door_tiles.append({"x": door_x + door_dx, "y": door_y + door_dy}) + + # Check if this stairs tile matches any door tile + for door_tile in door_tiles: + if stairs_tile_x == door_tile.x and stairs_tile_y == door_tile.y: + return true return false # Calculate safe position for stairs (3 tiles wide, 2 tiles tall) @@ -1331,11 +1517,11 @@ func _force_place_stairs(exit_room: Dictionary, grid: Array, tile_grid: Array, m # Try top wall (preferred) - use same logic as doors # Minimum room width: 2 (left buffer) + 3 (stairs) + 2 (right buffer) = 7 tiles if exit_room.w >= 7: - var min_x = exit_room.x + 2 # At least 2 tiles from left corner - var max_x = exit_room.x + exit_room.w - 5 # At least 5 tiles from right edge (2 buffer + 3 stairs) + var min_x = exit_room.x + 2 # At least 2 tiles from left corner + var max_x = exit_room.x + exit_room.w - 5 # At least 5 tiles from right edge (2 buffer + 3 stairs) # Try multiple positions to avoid doors for test_x in range(min_x, max_x + 1): - var test_stairs_start_x = test_x - 1 # Start 1 tile to the left (3 tiles wide) + var test_stairs_start_x = test_x - 1 # Start 1 tile to the left (3 tiles wide) if not stairs_overlaps_door.call(test_stairs_start_x, exit_room.y, stairs_w, stairs_h): stairs_x = test_stairs_start_x stairs_y = exit_room.y @@ -1366,7 +1552,7 @@ func _force_place_stairs(exit_room: Dictionary, grid: Array, tile_grid: Array, m stairs_w = 2 stairs_h = 3 for test_y in range(min_y, max_y + 1): - var test_stairs_start_y = test_y - 1 # Start 1 tile up (3 tiles tall) + var test_stairs_start_y = test_y - 1 # Start 1 tile up (3 tiles tall) if not stairs_overlaps_door.call(exit_room.x, test_stairs_start_y, stairs_w, stairs_h): stairs_x = exit_room.x stairs_y = test_stairs_start_y @@ -1441,18 +1627,45 @@ func _force_place_stairs(exit_room: Dictionary, grid: Array, tile_grid: Array, m var x = stairs_data.x + dx var y = stairs_data.y + dy if x >= 0 and x < map_size.x and y >= 0 and y < map_size.y: - grid[x][y] = 4 # Stairs - # Use simple stairs tiles (UP stairs style) + grid[x][y] = 4 # Stairs + # Render stairs tiles based on direction (same as normal placement) if stairs_dir == "UP": - if dy == 0: # First row - if dx == 1: # Middle tile - tile_grid[x][y] = Vector2i(10, 0) # Special middle tile for UP stairs + if dy == 0: # First row + if dx == 1: # Middle tile + tile_grid[x][y] = Vector2i(10, 0) # Special middle tile for UP stairs else: tile_grid[x][y] = STAIRS_UP_START + Vector2i(dx, dy) else: tile_grid[x][y] = STAIRS_UP_START + Vector2i(dx, dy) + elif stairs_dir == "DOWN": + # For DOWN stairs, use same tiles as door DOWN + # Row 0 (dy=0): use door tiles + # Row 1 (dy=1): use door tiles, except middle tile (col 1, row 1) which is 6,6 + if dy == 1 and dx == 1: # Second row, middle column (col 1, row 1) + tile_grid[x][y] = Vector2i(6, 6) # Special middle tile for DOWN stairs + else: + # Use door DOWN tiles (same as DOOR_BOTTOM_START = STAIRS_DOWN_START = 7,5) + tile_grid[x][y] = STAIRS_DOWN_START + Vector2i(dx, dy) + elif stairs_dir == "LEFT": + # Vertical stairs on left wall + if dx == 0: # First column + if dy == 1: # Middle tile (second row) + tile_grid[x][y] = Vector2i(5, 1) # Special middle tile for LEFT stairs + else: + tile_grid[x][y] = STAIRS_LEFT_START + Vector2i(dx, dy) + else: + tile_grid[x][y] = STAIRS_LEFT_START + Vector2i(dx, dy) + elif stairs_dir == "RIGHT": + # Vertical stairs on right wall + if dx == 1: # Second column + if dy == 1: # Middle tile (second row) + tile_grid[x][y] = Vector2i(11, 1) # Special middle tile for RIGHT stairs + else: + tile_grid[x][y] = STAIRS_RIGHT_START + Vector2i(dx, dy) + else: + tile_grid[x][y] = STAIRS_RIGHT_START + Vector2i(dx, dy) else: - # For other directions, use basic stairs tiles + # Fallback: use UP stairs tiles tile_grid[x][y] = STAIRS_UP_START + Vector2i(dx, dy) print("DungeonGenerator: Force placed ", stairs_dir, " stairs at tile (", stairs_data.x, ",", stairs_data.y, ") world pos: ", stairs_data.world_pos) @@ -1464,22 +1677,21 @@ func _place_interactable_objects_in_room(room: Dictionary, grid: Array, map_size # Medium rooms (9-10 tiles): 0-3 objects # Large rooms (11-12 tiles): 0-8 objects # Returns array of interactable object data dictionaries - var objects = [] var tile_size = 16 # Calculate room floor area (excluding walls) - var floor_w = room.w - 4 # Excluding 2-tile walls on each side + var floor_w = room.w - 4 # Excluding 2-tile walls on each side var floor_h = room.h - 4 var floor_area = floor_w * floor_h # Determine max objects based on room size var max_objects: int - if floor_area <= 16: # Small rooms (4x4 or smaller floor) + if floor_area <= 16: # Small rooms (4x4 or smaller floor) max_objects = 1 - elif floor_area <= 36: # Medium rooms (up to 6x6 floor) + elif floor_area <= 36: # Medium rooms (up to 6x6 floor) max_objects = 3 - else: # Large rooms (7x7+ floor) + else: # Large rooms (7x7+ floor) max_objects = 8 var num_objects = rng.randi_range(0, max_objects) @@ -1499,13 +1711,26 @@ func _place_interactable_objects_in_room(room: Dictionary, grid: Array, map_size var valid_positions = [] # Room interior is from room.x + 2 to room.x + room.w - 2 (excluding 2-tile walls) - for x in range(room.x + 2, room.x + room.w - 2): - for y in range(room.y + 2, room.y + room.h - 2): + # CRITICAL: Also exclude last row/column of floor tiles to prevent objects from spawning in walls + # Objects are 16x16, so we need at least 1 tile buffer from walls + # Floor tiles are at: room.x+2, room.x+3, ..., room.x+room.w-3 (last floor tile before wall) + # To exclude last tile, we want: room.x+2, room.x+3, ..., room.x+room.w-4 + var min_x = room.x + 2 + var max_x = room.x + room.w - 4 # Exclude last 2 floor tiles (room.w-3 is last floor, room.w-4 is second-to-last) + var 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) + + 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 < map_size.x and y >= 0 and y < map_size.y: # Check if it's a floor tile if grid[x][y] == 1: # Floor - var world_x = x * tile_size + tile_size / 2.0 - var world_y = y * tile_size + tile_size / 2.0 + # CRITICAL: Interactable objects are 16x16 pixels with origin at (0,0) (top-left) + # To center a 16x16 sprite in a 16x16 tile, we need to offset by 8 pixels into the tile + # Tile (x,y) spans from (x*16, y*16) to ((x+1)*16, (y+1)*16) + # Position sprite 8 pixels into the tile from top-left: (x*16 + 8, y*16 + 8) + var world_x = x * tile_size + 8 + var world_y = y * tile_size + 8 var world_pos = Vector2(world_x, world_y) # Check if position is valid (not blocked by door, not occupied by enemy) @@ -1585,19 +1810,19 @@ func _is_valid_interactable_position(world_pos: Vector2, all_doors: Array, all_e if tile_x >= door_x - 2 and tile_x < door_x and \ tile_y >= door_y - 1 and tile_y < door_y + door_h + 1: return false - else: # W + else: # W # Door on left wall, position should be to the right (inside room) if tile_x > door_x + door_w and tile_x <= door_x + door_w + 2 and \ tile_y >= door_y - 1 and tile_y < door_y + door_h + 1: return false - else: # N or S + else: # N or S # Vertical door if door.dir == "S": # Door on bottom wall, position should be above (inside room) if tile_y >= door_y - 2 and tile_y < door_y and \ tile_x >= door_x - 1 and tile_x < door_x + door_w + 1: return false - else: # N + else: # N # Door on top wall, position should be below (inside room) if tile_y > door_y + door_h and tile_y <= door_y + door_h + 2 and \ tile_x >= door_x - 1 and tile_x < door_x + door_w + 1: @@ -1613,3 +1838,622 @@ func _is_valid_interactable_position(world_pos: Vector2, all_doors: Array, all_e return false return true + +func _find_rooms_before_door(door: Dictionary, start_room: Dictionary, _all_rooms: Array, all_doors: Array) -> Array: + # Find rooms that are reachable from start WITHOUT going through this door + # This is used to place keys before KeyDoors + var rooms_before_door = [] + var visited = [start_room] + var queue = [start_room] + + while queue.size() > 0: + var current = queue.pop_front() + + # Add current room to result (it's reachable before the door) + # Don't include the rooms the door connects (need key before reaching them) + if current != door.room1 and current != door.room2: + if not rooms_before_door.has(current): + rooms_before_door.append(current) + + # Check all doors connected to current room (except the blocked door) + for d in all_doors: + if d == door: + continue # Skip the blocked door + + var next_room = null + if d.room1 == current: + next_room = d.room2 + elif d.room2 == current: + next_room = d.room1 + + if next_room != null and not visited.has(next_room): + visited.append(next_room) + queue.append(next_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: + # Place blocking doors on existing tile doors + # Returns array of blocking door data dictionaries + var blocking_doors = [] + var tile_size = 16 + + # Get start and exit room references + var start_room = all_rooms[start_room_index] + var _exit_room = all_rooms[exit_room_index] + + # Calculate reachability from start room to determine where keys can be placed + var _reachable_rooms_from_start = _find_reachable_rooms(start_room, all_rooms, all_doors) + + # Track which rooms have puzzles and which doors are already assigned + var _rooms_with_puzzles = {} # room -> true + var assigned_doors = [] # Doors already assigned to a room puzzle + var room_puzzle_data = {} # room -> {type: "switch" or "enemy", doors: []} + + # 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 + for i in range(all_rooms.size()): + if i == start_room_index or i == exit_room_index: + continue # Skip start and exit rooms + + var room = all_rooms[i] + + if rng.randf() < puzzle_room_chance: + # 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 = [] + 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 + # Compare rooms by position and size (value comparison, not reference) + var door_leads_out_of_this_room = (door_room1.x == room.x and door_room1.y == room.y and \ + door_room1.w == room.w and door_room1.h == room.h) + + if door_leads_out_of_this_room: + # CRITICAL: Also check that this door doesn't lead into start or exit room + if not "room2" in door or not door.room2 or door.room2.is_empty(): + continue + + var door_room2 = door.room2 + var door_room2_index = -1 + for j in range(all_rooms.size()): + var check_room = all_rooms[j] + if check_room.x == door_room2.x and check_room.y == door_room2.y and \ + check_room.w == door_room2.w and check_room.h == door_room2.h: + door_room2_index = j + break + + # Skip if door leads into start or exit room + if door_room2_index == start_room_index or door_room2_index == exit_room_index: + continue + + doors_out_of_room.append(door) + + if doors_out_of_room.size() == 0: + continue # No doors leading out of this room, skip + + # Decide puzzle type: 33% walk switch, 33% pillar switch, 33% enemy spawner (if room is large enough) + var can_have_enemies = false + var interior_width = room.w - 4 # Exclude 2-tile walls + var interior_height = room.h - 4 + can_have_enemies = (interior_width >= 5 and interior_height >= 5) + + var puzzle_type = "" + var rand_val = rng.randf() + if can_have_enemies and rand_val < 0.33: + puzzle_type = "enemy" + elif rand_val < 0.66: + puzzle_type = "switch_walk" + else: + puzzle_type = "switch_pillar" + + # Store puzzle data for this room + room_puzzle_data[room] = { + "type": puzzle_type, + "doors": doors_out_of_room + } + + # Mark these doors as assigned + for door in doors_out_of_room: + assigned_doors.append(door) + + # 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! + # When you enter the puzzle room, these doors close, trapping you until you solve the puzzle + for room in room_puzzle_data.keys(): + # CRITICAL SAFETY CHECK #1: Verify this room is actually in room_puzzle_data + if not room in room_puzzle_data: + push_error("DungeonGenerator: ERROR - Room (", room.x, ", ", room.y, ") is NOT in room_puzzle_data! This should never happen!") + continue + + # CRITICAL SAFETY CHECK #2: Never create blocking doors for start or exit rooms + var room_index = -1 + for j in range(all_rooms.size()): + var check_room = all_rooms[j] + if check_room.x == room.x and check_room.y == room.y and \ + check_room.w == room.w and check_room.h == room.h: + room_index = j + break + + if room_index == start_room_index or room_index == exit_room_index: + push_error("DungeonGenerator: ERROR - Attempted to create blocking doors for start/exit room! Skipping.") + continue + + # CRITICAL SAFETY CHECK #3: Verify this room is actually in all_rooms (sanity check) + if room_index == -1: + push_error("DungeonGenerator: ERROR - Room (", room.x, ", ", room.y, ") not found in all_rooms! Skipping.") + continue + + var puzzle_info = room_puzzle_data[room] + var doors_in_room = puzzle_info.doors # Doors that are IN this puzzle room (lead OUT OF it) + var puzzle_type = puzzle_info.type + + if doors_in_room.size() == 0: + print("DungeonGenerator: WARNING - Room has puzzle but no doors! Skipping.") + continue + + # Randomly choose door type: 50% StoneDoor, 50% GateDoor + var door_type = "StoneDoor" if rng.randf() < 0.5 else "GateDoor" + + # Create puzzle element first (switch or spawner) - ONCE per room, shared by all doors + var puzzle_element_created = false + var puzzle_element_data = {} + + if puzzle_type == "switch_walk" or puzzle_type == "switch_pillar": + # Find a valid floor position for switch IN THE PUZZLE ROOM + var switch_type = "walk" if puzzle_type == "switch_walk" else "pillar" + var switch_weight = 1.0 if switch_type == "walk" else 5.0 + + var switch_data = _find_floor_switch_position(room, grid, map_size, rng, -1, -1) + if switch_data != null and not switch_data.is_empty() and switch_data.has("position"): + puzzle_element_created = true + puzzle_element_data = { + "type": "switch", + "switch_type": switch_type, + "switch_weight": switch_weight, + "switch_data": switch_data, + "switch_room": room + } + print("DungeonGenerator: Created switch puzzle for room (", room.x, ", ", room.y, ") - type: ", switch_type) + else: + print("DungeonGenerator: WARNING - Could not place floor switch in puzzle room (", room.x, ", ", room.y, ")! Skipping puzzle.") + elif puzzle_type == "enemy": + # Add enemy spawner IN THE PUZZLE ROOM + var spawner_positions = [] + for x in range(room.x + 2, room.x + room.w - 2): + for y in range(room.y + 2, room.y + room.h - 2): + if x >= 0 and x < map_size.x and y >= 0 and y < map_size.y: + if grid[x][y] == 1: # Floor + var world_x = x * tile_size + tile_size / 2.0 + var world_y = y * tile_size + tile_size / 2.0 + spawner_positions.append({ + "position": Vector2(world_x, world_y), + "tile_x": x, + "tile_y": y + }) + + if spawner_positions.size() > 0: + spawner_positions.shuffle() + var spawner_data = spawner_positions[0] + puzzle_element_created = true + puzzle_element_data = { + "type": "enemy", + "spawner_data": spawner_data, + "spawner_room": room + } + print("DungeonGenerator: Created enemy spawner puzzle for room (", room.x, ", ", room.y, ") - spawner at ", spawner_data.position) + else: + print("DungeonGenerator: WARNING - Could not place enemy spawner in puzzle room (", room.x, ", ", room.y, ")! Skipping puzzle.") + + # CRITICAL SAFETY CHECK: Only create blocking doors if puzzle element was successfully created + if not puzzle_element_created: + push_error("DungeonGenerator: ERROR - Puzzle element was NOT created for room (", room.x, ", ", room.y, ") with puzzle_type: ", puzzle_type, "! Skipping ALL doors in this room.") + # Remove doors from assigned list since we're not creating the puzzle + for door in doors_in_room: + if door in assigned_doors: + assigned_doors.erase(door) + continue + + # CRITICAL: Verify puzzle_element_data is valid before proceeding + if puzzle_element_data.is_empty() or not puzzle_element_data.has("type"): + push_error("DungeonGenerator: ERROR - puzzle_element_data is invalid for room (", room.x, ", ", room.y, ")! puzzle_element_created was true but data is empty!") + continue + + # Create blocking doors for at least 1 door (minimum), or all doors in the room + # For now, create blocking doors for ALL doors in the puzzle room + print("DungeonGenerator: Creating blocking doors for room (", room.x, ", ", room.y, ") with ", doors_in_room.size(), " doors, puzzle type: ", puzzle_type, ", puzzle_element type: ", puzzle_element_data.type) + 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 + + 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) + + 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 + + # 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 + 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 + + 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) + "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) + "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) + + # 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 + var door_world_x = open_tile_x * tile_size + tile_size / 2.0 + var door_world_y = open_tile_y * tile_size + tile_size / 2.0 + + # Create door data + # Position is the OPEN state position (will move to CLOSED when entering room) + # CRITICAL: Verify room is still a valid puzzle room before creating door + if not room in room_puzzle_data: + push_error("DungeonGenerator: ERROR - Room (", room.x, ", ", room.y, ") is no longer in room_puzzle_data! Cannot create door.") + continue + + # NOTE: door_room1 is already declared at line 1933 and verified to match puzzle room at line 1935-1940 + # No need to re-declare or re-verify here - the door_in_puzzle_room check above already ensures room1 matches + + var door_data = { + "type": door_type, + "direction": direction, + "position": Vector2(door_world_x, door_world_y), # OPEN position (tile center) + "tile_x": open_tile_x, + "tile_y": open_tile_y, + "door": door, + "blocking_room": room, # CRITICAL: This door is IN the puzzle room (the room that has the puzzle) + "is_closed": false, # Start open, close when entering puzzle room + "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 + + 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, ")") + + # 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 + var door_has_valid_puzzle = false + if puzzle_element_data.has("type") and puzzle_element_data.type == "switch": + if puzzle_element_data.has("switch_data") and puzzle_element_data.switch_data.has("position"): + door_data.floor_switch_position = puzzle_element_data.switch_data.position + door_data.switch_tile_x = puzzle_element_data.switch_data.tile_x + door_data.switch_tile_y = puzzle_element_data.switch_data.tile_y + door_data.switch_room = puzzle_element_data.switch_room + door_data.requires_switch = true + door_data.switch_type = puzzle_element_data.switch_type + door_data.switch_required_weight = puzzle_element_data.switch_weight + door_has_valid_puzzle = true + print("DungeonGenerator: Added switch data to door - switch at (", door_data.switch_tile_x, ", ", door_data.switch_tile_y, ")") + elif puzzle_element_data.has("type") and puzzle_element_data.type == "enemy": + if puzzle_element_data.has("spawner_data") and puzzle_element_data.spawner_data.has("position"): + if not "enemy_spawners" in door_data: + door_data.enemy_spawners = [] + door_data.enemy_spawners.append({ + "position": puzzle_element_data.spawner_data.position, + "tile_x": puzzle_element_data.spawner_data.tile_x, + "tile_y": puzzle_element_data.spawner_data.tile_y, + "room": puzzle_element_data.spawner_room, + "spawn_once": true # Only spawn 1 enemy, then destroy spawner + }) + door_data.requires_enemies = true + door_has_valid_puzzle = true + print("DungeonGenerator: Added enemy spawner data to door - spawner at (", puzzle_element_data.spawner_data.tile_x, ", ", puzzle_element_data.spawner_data.tile_y, ")") + + # CRITICAL SAFETY CHECK: Only add door to blocking doors if it has valid puzzle element + if not door_has_valid_puzzle: + push_error("DungeonGenerator: ERROR - Blocking door for room (", room.x, ", ", room.y, ") has no valid puzzle element! Skipping door. puzzle_type: ", puzzle_type, ", puzzle_element_data: ", puzzle_element_data) + continue # Skip this door - don't add it to blocking_doors + + # FINAL SAFETY CHECK: Verify door has either requires_switch or requires_enemies set + if door_data.type == "StoneDoor" or door_data.type == "GateDoor": + var has_switch = door_data.get("requires_switch", false) == true + var has_enemies = door_data.get("requires_enemies", false) == true + if not has_switch and not has_enemies: + push_error("DungeonGenerator: ERROR - Blocking door (StoneDoor/GateDoor) has neither requires_switch nor requires_enemies! Door data: ", door_data.keys(), " - SKIPPING DOOR") + continue # Skip this door - it's invalid + + # FINAL CRITICAL SAFETY CHECK: Verify door's blocking_room matches the puzzle room exactly + if door_data.blocking_room.x != room.x or door_data.blocking_room.y != room.y or \ + door_data.blocking_room.w != room.w or door_data.blocking_room.h != room.h: + 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 + 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, ")") + + # STEP 3: Randomly assign some doors as KeyDoors (except start/exit room doors and already assigned doors) + var key_door_chance = 0.2 # 20% chance per door + var key_doors_to_create = [] + + for door in all_doors: + # Skip if already assigned to a room puzzle + if door in assigned_doors: + continue + + # Skip doors connected to start or exit rooms + var door_room1 = door.room1 if "room1" in door else null + var door_room2 = door.room2 if "room2" in door else null + + var is_start_or_exit_door = false + if door_room1: + var room1_index = all_rooms.find(door_room1) + if room1_index == start_room_index or room1_index == exit_room_index: + is_start_or_exit_door = true + if door_room2: + var room2_index = all_rooms.find(door_room2) + if room2_index == start_room_index or room2_index == exit_room_index: + is_start_or_exit_door = true + + if is_start_or_exit_door: + continue + + if rng.randf() < key_door_chance: + key_doors_to_create.append(door) + + # STEP 4: Create KeyDoors with keys placed BEFORE the keydoor + for door in key_doors_to_create: + # Determine direction + var direction = "" + if "dir" in door: + 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 middle tile position + var door_tile_x = door.x + var door_tile_y = door.y + var middle_tile_x = door_tile_x + var middle_tile_y = door_tile_y + + match direction: + "Down": + # Door Down (3x2): KeyDoors should be placed on row 0, col 1 (CLOSED state) + # Row 0 is the upper row (closer to room interior) - this is where KeyDoors start CLOSED + # When opened with key, they move to row 1 (col 1, row 1) - 16px down + # BUT: After 180° rotation, we need to adjust Y position UP by 8 pixels (half a tile) + # to account for sprite alignment - position will be adjusted in door.gd + middle_tile_x = door_tile_x + 1 # col 1 (middle column) + middle_tile_y = door_tile_y + 0 # row 0 (upper row - CLOSED state for KeyDoor) + "Up": + # Door Up (3x2): door spans 2 tiles tall, wall is at top edge + # Use the TOP tile (row 0) AT the wall boundary + middle_tile_x = door_tile_x + 1 # col 1 (middle column) + middle_tile_y = door_tile_y + 1 # row 0 (TOP tile, AT the wall boundary) + "Right": + # Door Right (2x3): door spans 2 tiles, wall is at right edge + # Use the RIGHT tile (col 1) at the wall boundary, not the left tile + middle_tile_x = door_tile_x + 0 # col 1 (right column, AT the wall boundary) + middle_tile_y = door_tile_y + 1 # row 1 (middle row) + "Left": + # Door Left (2x3): door spans 2 tiles, wall is at left edge + # Use the LEFT tile (col 0) at the wall boundary + middle_tile_x = door_tile_x + 1 # col 0 (left column, AT the wall boundary) + middle_tile_y = door_tile_y + 1 # row 1 (middle row) + + var door_world_x = middle_tile_x * tile_size + tile_size / 2.0 + var door_world_y = middle_tile_y * tile_size + tile_size / 2.0 + + # Determine which room this door blocks (the room you're entering into) + var door_room1 = door.room1 if "room1" in door else null + var door_room2 = door.room2 if "room2" in door else null + var blocking_room = door_room2 if door_room2 != null else door_room1 + + # Find rooms reachable BEFORE this door (for key placement) + var rooms_before_door = _find_rooms_before_door(door, start_room, all_rooms, all_doors) + + # Pick a room for the key (must be reachable before the door) + var key_room = null + if rooms_before_door.size() > 0: + # Exclude start and exit rooms from key placement + var key_room_candidates = [] + for room in rooms_before_door: + var room_index = all_rooms.find(room) + if room_index != start_room_index and room_index != exit_room_index: + key_room_candidates.append(room) + + if key_room_candidates.size() > 0: + key_room = key_room_candidates[rng.randi() % key_room_candidates.size()] + else: + # Fallback: use start room + key_room = start_room + else: + # Fallback: use start room + key_room = start_room + + var door_data = { + "type": "KeyDoor", + "direction": direction, + "position": Vector2(door_world_x, door_world_y), + "tile_x": middle_tile_x, + "tile_y": middle_tile_y, + "door": door, + "blocking_room": blocking_room, + "is_closed": true, # KeyDoors always closed + "key_room": key_room # Room where key is placed (before this door) + } + + blocking_doors.append(door_data) + + return blocking_doors + +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 + # exclude_door_x, exclude_door_y: Tile coordinates of door to avoid placing switch too close to + # Returns a dictionary with position (Vector2) and tile_x, tile_y (int) or empty dict if no position found + var tile_size = 16 + var valid_positions = [] + var min_distance_from_door = 3 # Minimum tiles away from door + + # Room interior is from room.x + 2 to room.x + room.w - 2 (excluding 2-tile walls) + # Also exclude door tiles (grid value 2) to avoid placing switches in doorways + for x in range(room.x + 2, room.x + room.w - 2): + for y in range(room.y + 2, room.y + room.h - 2): + if x >= 0 and x < map_size.x and y >= 0 and y < map_size.y: + # Check if it's a floor tile (not a door tile) + if grid[x][y] == 1: # Floor (not door which is 2) + # Check if position is too close to door (if door position provided) + var too_close_to_door = false + if exclude_door_x >= 0 and exclude_door_y >= 0: + var distance_x = abs(x - exclude_door_x) + var distance_y = abs(y - exclude_door_y) + var distance = max(distance_x, distance_y) # Chebyshev distance (tiles) + if distance < min_distance_from_door: + too_close_to_door = true + + if not too_close_to_door: + var world_x = x * tile_size + tile_size / 2.0 + var world_y = y * tile_size + tile_size / 2.0 + valid_positions.append({ + "position": Vector2(world_x, world_y), + "tile_x": x, + "tile_y": y + }) + + if valid_positions.size() > 0: + # Pick a random position + return valid_positions[rng.randi() % valid_positions.size()] + + return {} + +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. + if not "room1" in door or not "room2" in door: + return "Up" # Default + + var room1 = door.room1 + 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) + + var dx = room2.x - room1.x + var dy = room2.y - room1.y + + # Check which direction has the larger difference + if abs(dy) > abs(dx): + # Vertical alignment + if dy < 0: + return "Up" # room2 is above room1 + else: + return "Down" # room2 is below room1 + else: + # Horizontal alignment + if dx < 0: + return "Left" # room2 is left of room1 + else: + return "Right" # room2 is right of room1 diff --git a/src/scripts/enemy_base.gd b/src/scripts/enemy_base.gd index ba24870..d7f0ead 100644 --- a/src/scripts/enemy_base.gd +++ b/src/scripts/enemy_base.gd @@ -44,6 +44,11 @@ func _ready(): # Top-down physics motion_mode = MOTION_MODE_FLOATING + + # CRITICAL: Set collision mask to include interactable objects (layer 2) and walls (layer 7) + # This allows enemies to collide with interactable objects so they can path around them + # Walls are on layer 7 (bit 6 = 64), not layer 4! + collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64) func _physics_process(delta): if is_dead: @@ -90,6 +95,9 @@ func _physics_process(delta): # Check collisions with players _check_player_collision() + # Check collisions with interactable objects + _check_interactable_object_collision() + # Sync position and animation to clients (only server sends) if multiplayer.has_multiplayer_peer() and is_multiplayer_authority(): # Get state value if enemy has a state variable (for bats/slimes) @@ -125,6 +133,60 @@ func _check_player_collision(): if collider and collider.is_in_group("player"): _attack_player(collider) +func _check_interactable_object_collision(): + # Check collisions with interactable objects and handle pathfinding around them + var blocked_objects = [] + + for i in get_slide_collision_count(): + var collision = get_slide_collision(i) + var collider = collision.get_collider() + + if collider and collider.is_in_group("interactable_object"): + var obj = collider + + # CRITICAL: Enemies cannot move objects that cannot be lifted + # If object is not liftable, enemy should try to path around it + if obj.has_method("can_be_lifted") and not obj.can_be_lifted(): + # Object cannot be lifted - store for pathfinding + blocked_objects.append({"object": obj, "collision": collision}) + # If object is liftable but not currently being held, we can still try to push it + # but enemies don't actively push liftable objects (only players do) + elif obj.has_method("is_being_held") and obj.is_being_held(): + # Object is being held by someone - treat as obstacle + blocked_objects.append({"object": obj, "collision": collision}) + + # Handle pathfinding around blocked objects + if blocked_objects.size() > 0 and not is_knocked_back: + var collision_normal = blocked_objects[0].collision.get_normal() + var _obj_pos = blocked_objects[0].object.global_position + + # Try to path around the object by moving perpendicular to collision normal + # This creates a side-stepping behavior to go around obstacles + var perpendicular = Vector2(-collision_normal.y, collision_normal.x) # Rotate 90 degrees + + # Choose perpendicular direction that moves toward target (if we have one) + if target_player and is_instance_valid(target_player): + var to_target = (target_player.global_position - global_position).normalized() + # If perpendicular dot product with target direction is negative, flip it + if perpendicular.dot(to_target) < 0: + perpendicular = -perpendicular + + # Apply perpendicular movement (side-step around object) + var side_step_velocity = perpendicular * move_speed * 0.7 # 70% of move speed for side-step + velocity = velocity.lerp(side_step_velocity, 0.3) # Smoothly blend with existing velocity + + # Also add some push-away from object to create clearance + var push_away = collision_normal * move_speed * 0.3 + velocity = velocity + push_away + + # Limit total velocity to move_speed + if velocity.length() > move_speed: + velocity = velocity.normalized() * move_speed + + # For humanoid enemies, sometimes try to destroy the object + if has_method("_try_attack_object") and randf() < 0.1: # 10% chance per frame when blocked + call("_try_attack_object", blocked_objects[0].object) + func _attack_player(player): # Attack cooldown if attack_timer > 0: @@ -199,6 +261,11 @@ func take_damage(amount: float, from_position: Vector2): # Flash red (even if dying, show the hit) _flash_damage() + # Show damage number (red, using dmg_numbers.png font) above enemy + # Only show if damage > 0 + if amount > 0: + _show_damage_number(amount, from_position) + # Sync damage visual to clients # Use game_world to route damage visual sync instead of direct RPC to avoid node path issues if multiplayer.has_multiplayer_peer() and is_inside_tree(): @@ -221,6 +288,44 @@ func rpc_take_damage(amount: float, from_position: Vector2): if is_multiplayer_authority(): take_damage(amount, from_position) +func _show_damage_number(amount: float, from_position: Vector2): + # Show damage number (red, using dmg_numbers.png font) above enemy + # Only show if damage > 0 + if amount <= 0: + return + + var damage_number_scene = preload("res://scenes/damage_number.tscn") + if not damage_number_scene: + return + + var damage_label = damage_number_scene.instantiate() + if not damage_label: + return + + # Set damage text and red color + damage_label.label = str(int(amount)) + damage_label.color = Color.RED + + # Calculate direction from attacker (slight upward variation) + var direction_from_attacker = (global_position - from_position).normalized() + # Add slight upward bias + direction_from_attacker = direction_from_attacker.lerp(Vector2(0, -1), 0.5).normalized() + damage_label.direction = direction_from_attacker + + # Position above enemy's head + var game_world = get_tree().get_first_node_in_group("game_world") + if game_world: + var entities_node = game_world.get_node_or_null("Entities") + if entities_node: + entities_node.add_child(damage_label) + damage_label.global_position = global_position + Vector2(0, -16) # Above enemy head + else: + get_tree().current_scene.add_child(damage_label) + damage_label.global_position = global_position + Vector2(0, -16) + else: + get_tree().current_scene.add_child(damage_label) + damage_label.global_position = global_position + Vector2(0, -16) + func _flash_damage(): # Flash red visual effect if sprite: diff --git a/src/scripts/enemy_bat.gd b/src/scripts/enemy_bat.gd index 1133f17..1ed1f9a 100644 --- a/src/scripts/enemy_bat.gd +++ b/src/scripts/enemy_bat.gd @@ -21,6 +21,9 @@ func _ready(): damage = 5.0 state_timer = idle_duration + + # CRITICAL: Ensure collision mask is set correctly (walls are on layer 7 = bit 6 = 64) + collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64) func _physics_process(delta): # Always update animation (even when dead, and on clients) diff --git a/src/scripts/enemy_humanoid.gd b/src/scripts/enemy_humanoid.gd index 99aec4f..406c61c 100644 --- a/src/scripts/enemy_humanoid.gd +++ b/src/scripts/enemy_humanoid.gd @@ -107,6 +107,9 @@ var spawn_position: Vector2 func _ready(): super._ready() + # CRITICAL: Ensure collision mask is set correctly (walls are on layer 7 = bit 6 = 64) + collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64) + # Override sprite reference (we use layered sprites, not single sprite) sprite = null # Don't use base class sprite @@ -364,9 +367,13 @@ func _physics_process(delta): if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority(): return - # Update attack timer + # Update attack timer and reset attack flags when cooldown is over if attack_timer > 0: attack_timer -= delta + if attack_timer <= 0: + # Attack cooldown finished - reset attack flags + can_attack = true + is_attacking = false # Handle knockback if is_knocked_back: @@ -551,10 +558,23 @@ func _chasing_behavior(_delta): # Chase player (get close enough to attack) var desired_distance = 45.0 # Stop this far from player (attack range) if dist > desired_distance: + # Still too far - chase player velocity = to_player * move_speed * 0.8 # Reduce chase speed to 80% (was 100%) else: - # Already close enough, stop and wait for attack cooldown - velocity = Vector2.ZERO + # Close enough to attack - but only stop if we can attack soon + # If attack is on cooldown, keep following at reduced speed to maintain distance + if can_attack: + # Can attack - stop and wait for attack opportunity + velocity = Vector2.ZERO + else: + # Attack on cooldown - keep moving slowly to maintain position + # Move slightly away if too close, or maintain distance + if dist < desired_distance * 0.8: + # Too close - back away slightly + velocity = -to_player * move_speed * 0.3 + else: + # Good distance - just face player + velocity = Vector2.ZERO current_direction = _get_direction_from_vector(to_player) # Set animation based on movement @@ -592,7 +612,9 @@ func _attacking_behavior(delta): return # Don't return to chasing yet # Return to chasing after attack completes - if state_timer <= 0 and not is_attacking and not is_charging_attack: + # Check if attack animation is done (not in SWORD animation anymore) and cooldown is over + var attack_animation_done = (current_animation != "SWORD") + if state_timer <= 0 and attack_animation_done and not is_charging_attack: ai_state = AIState.CHASING state_timer = 3.0 @@ -665,6 +687,9 @@ func _perform_attack(): is_attacking = true is_charging_attack = false # Reset charging flag + # CRITICAL: Set attack timer for cooldown (this will reset can_attack when it expires) + attack_timer = attack_cooldown + # Play attack animation _set_animation("SWORD") @@ -701,16 +726,42 @@ func _perform_attack(): # Spawn sword projectile (only on server/authority) if sword_projectile_scene and is_multiplayer_authority(): var projectile = sword_projectile_scene.instantiate() - get_parent().add_child(projectile) - projectile.setup(attack_direction, self) - var spawn_offset = attack_direction * 10.0 - projectile.global_position = global_position + spawn_offset - print(name, " attacked with sword projectile at ", global_position) + if projectile: + # CRITICAL: Setup projectile with direction and owner BEFORE adding to scene + projectile.setup(attack_direction, self) + var spawn_offset = attack_direction * 10.0 + projectile.global_position = global_position + spawn_offset + + # Add to scene tree + var parent = get_parent() + if parent: + parent.add_child(projectile) + else: + push_error("EnemyHumanoid: ERROR - No parent node to add projectile to!") + projectile.queue_free() + +func _try_attack_object(obj: Node): + # Humanoid enemies can sometimes try to attack/destroy interactable objects + # Only try if we're not already attacking and object is destroyable + if is_attacking or not can_attack: + return - # Reset attack cooldown - await get_tree().create_timer(attack_cooldown).timeout - can_attack = true - is_attacking = false + # Only try on server/authority + if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority(): + return + + # Check if object can be destroyed + if obj.has_method("can_be_destroyed") and obj.can_be_destroyed(): + # 30% chance to try attacking the object (less frequent than player attacks to avoid spam) + if randf() < 0.3: + # Face the object + var to_object = (obj.global_position - global_position).normalized() + current_direction = _get_direction_from_vector(to_object) + + # Perform attack - sword projectile will damage the object if it hits + # The object will handle damage from sword projectiles (sword_projectile.gd already handles this) + _perform_attack() + print(name, " is attacking object ", obj.name, "!") @rpc("authority", "reliable") func _sync_attack(direction: int, attack_dir: Vector2): @@ -739,6 +790,7 @@ func _update_animation(delta): # Update animation frame timing (even when dead, to play death animation) time_since_last_frame += delta if time_since_last_frame >= ANIMATIONS[current_animation]["frameDurations"][current_frame] / 1000.0: + var was_attacking = (current_animation == "SWORD" and current_frame == len(ANIMATIONS[current_animation]["frames"]) - 1) current_frame += 1 if current_frame >= len(ANIMATIONS[current_animation]["frames"]): current_frame -= 1 # Stay on last frame @@ -747,7 +799,15 @@ func _update_animation(delta): if ANIMATIONS[current_animation]["nextAnimation"] != null and not is_dead: # Don't transition to next animation if dead current_frame = 0 + var old_animation = current_animation current_animation = ANIMATIONS[current_animation]["nextAnimation"] + + # CRITICAL: If SWORD animation just completed, reset attack flags + if old_animation == "SWORD" and was_attacking: + # Attack animation finished - reset attack state + # Note: can_attack will be reset when attack_timer expires + # But we can reset is_attacking now since animation is done + is_attacking = false time_since_last_frame = 0.0 # Calculate frame index (8 directions, 35 frames per direction) diff --git a/src/scripts/enemy_rat.gd b/src/scripts/enemy_rat.gd index 39f5222..fafcec0 100644 --- a/src/scripts/enemy_rat.gd +++ b/src/scripts/enemy_rat.gd @@ -19,6 +19,9 @@ func _ready(): damage = 8.0 state_timer = idle_duration + + # CRITICAL: Ensure collision mask is set correctly (walls are on layer 7 = bit 6 = 64) + collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64) func _ai_behavior(delta): # Update state timer diff --git a/src/scripts/enemy_slime.gd b/src/scripts/enemy_slime.gd index 5175f83..48006bb 100644 --- a/src/scripts/enemy_slime.gd +++ b/src/scripts/enemy_slime.gd @@ -27,11 +27,15 @@ func _ready(): max_health = 20.0 current_health = max_health - move_speed = 35.0 # Slow normally (reduced from 60) + move_speed = 20.0 # Very slow (reduced from 35) damage = 6.0 state_timer = idle_duration + # CRITICAL: Ensure collision mask is set correctly after super._ready() + # Walls are on layer 7 (bit 6 = 64), objects on layer 2, players on layer 1 + collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64) + # Slime is small - adjust collision if collision_shape and collision_shape.shape: collision_shape.shape.radius = 6.0 # 12x12 effective size @@ -120,11 +124,11 @@ func _start_jump(): # Jump towards player if nearby if target_player and is_instance_valid(target_player): var direction = (target_player.global_position - global_position).normalized() - velocity = direction * (move_speed * 1.8) # Faster during jump + velocity = direction * (move_speed * 1.4) # Slightly faster during jump (reduced from 1.8) else: # Random jump direction var random_dir = Vector2(randf() - 0.5, randf() - 0.5).normalized() - velocity = random_dir * (move_speed * 1.8) + velocity = random_dir * (move_speed * 1.4) # Slightly faster during jump (reduced from 1.8) func _jumping_behavior(_delta): # Continue moving in jump direction diff --git a/src/scripts/enemy_spawner.gd b/src/scripts/enemy_spawner.gd index 977dd94..2602e43 100644 --- a/src/scripts/enemy_spawner.gd +++ b/src/scripts/enemy_spawner.gd @@ -16,10 +16,16 @@ func _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(" spawn_on_ready: ", spawn_on_ready) print(" max_enemies: ", max_enemies) + print(" enemy_scenes.size(): ", enemy_scenes.size()) print(" Parent: ", get_parent()) + # Verify enemy_scenes is set + if enemy_scenes.size() == 0: + push_error("EnemySpawner: ERROR - enemy_scenes array is EMPTY! Spawner will not be able to spawn enemies!") + # Spawn on server, or in single player (no multiplayer peer) var should_spawn = spawn_on_ready and (multiplayer.is_server() or not multiplayer.has_multiplayer_peer()) print(" Should spawn? ", should_spawn) @@ -28,7 +34,7 @@ func _ready(): print(" Calling spawn_enemy()...") call_deferred("spawn_enemy") # Use call_deferred to ensure scene is ready else: - print(" NOT spawning - conditions not met") + print(" NOT spawning - conditions not met (spawn_on_ready=", spawn_on_ready, ", will spawn when player enters room)") print("========================================") func _process(delta): @@ -48,6 +54,23 @@ func _process(delta): func spawn_enemy(): print(">>> spawn_enemy() CALLED <<<") + print(" Spawner: ", name, " at ", global_position) + print(" enemy_scenes.size(): ", enemy_scenes.size()) + print(" spawned_enemies.size(): ", spawned_enemies.size(), " / max_enemies: ", max_enemies) + print(" spawn_on_ready: ", spawn_on_ready) + + # CRITICAL: Check if we can spawn (don't spawn if already at max) + # Clean up dead enemies first + spawned_enemies = spawned_enemies.filter(func(e): return is_instance_valid(e) and not e.is_dead) + + if spawned_enemies.size() >= max_enemies: + print(" ERROR: Cannot spawn - already at max enemies (", spawned_enemies.size(), " >= ", max_enemies, ")") + return + + # Only spawn on server (authority) + if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority(): + print(" ERROR: Cannot spawn - not multiplayer authority!") + return # Choose enemy scene to spawn var scene_to_spawn: PackedScene = null @@ -55,15 +78,39 @@ func spawn_enemy(): # Use random scene from list scene_to_spawn = enemy_scenes[randi() % enemy_scenes.size()] print(" Selected enemy scene from list: ", scene_to_spawn) + else: + push_error("ERROR: enemy_scenes array is EMPTY! Spawner has no enemy scenes to spawn!") + return if not scene_to_spawn: - push_error("ERROR: No enemy scene set for spawner! Add scenes to enemy_scenes array.") + push_error("ERROR: Failed to select enemy scene!") return print(" Spawning enemy at ", global_position) - # Spawn smoke puff effect - _spawn_smoke_puff() + # CRITICAL: Spawn 3-4 smoke puffs first, wait for them to finish, THEN spawn enemy + var num_puffs = randi_range(3, 4) # 3 or 4 smoke puffs + print(" Spawning ", num_puffs, " smoke puffs before 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 + + 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) + if puff: + smoke_puffs.append(puff) + + # 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 + await get_tree().create_timer(smoke_animation_duration).timeout + + print(" Smoke puffs finished - now spawning enemy...") print(" Instantiating enemy scene...") var enemy = scene_to_spawn.instantiate() @@ -79,6 +126,18 @@ func spawn_enemy(): enemy.spawn_position = global_position print(" Set enemy position to: ", global_position) + # 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) + + # 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) + + # Set multiplayer authority BEFORE adding to scene tree (CRITICAL for RPC to work!) + if multiplayer.has_multiplayer_peer(): + enemy.set_multiplayer_authority(1) + # Add to YSort node for automatic Y-sorting var ysort = get_parent().get_node_or_null("Entities") var parent = ysort if ysort else get_parent() @@ -91,9 +150,9 @@ func spawn_enemy(): print(" Adding enemy as child...") parent.add_child(enemy) - # Set multiplayer authority to server (peer 1) - if multiplayer.has_multiplayer_peer(): - enemy.set_multiplayer_authority(1) + # CRITICAL: Re-verify collision_mask AFTER adding to scene (in case _ready() or scene file overrides it) + # Use call_deferred to ensure _ready() has completed first, then set the entire mask + call_deferred("_verify_enemy_collision_mask", enemy) # Determine which scene index was used (for syncing to clients) var scene_index = -1 @@ -112,18 +171,32 @@ func spawn_enemy(): print(" ✓ Successfully spawned enemy: ", enemy.name, " at ", global_position, " scene_index: ", scene_index) print(" Total spawned enemies: ", spawned_enemies.size()) + # If this spawner is marked for one-time spawn, destroy it after spawning + if has_meta("spawn_once") and get_meta("spawn_once"): + print(" Spawner marked for one-time spawn - destroying after spawn") + call_deferred("queue_free") # Destroy spawner after spawning once + # Sync spawn to all clients via GameWorld if multiplayer.has_multiplayer_peer() and multiplayer.is_server(): - # Get GameWorld directly since spawner is a child of GameWorld - var game_world = get_parent() - print(" DEBUG: game_world=", game_world, " spawner name=", name) + # Get GameWorld by traversing up the tree (spawner is child of Entities, which is child of GameWorld) + 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_enemy_spawn"): + game_world = node + break + node = node.get_parent() + if game_world and game_world.has_method("_sync_enemy_spawn"): - # Use spawner name (relative to GameWorld) since it's a direct child + # 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) else: - push_error("ERROR: Could not find GameWorld or _sync_enemy_spawn method! game_world=", game_world, " has_method=", game_world.has_method("_sync_enemy_spawn") if game_world else "N/A") + var has_method_str = str(game_world.has_method("_sync_enemy_spawn")) if game_world else "N/A" + push_error("ERROR: Could not find GameWorld or _sync_enemy_spawn method! game_world=", game_world, " has_method=", has_method_str) func spawn_enemy_at_position(spawn_pos: Vector2, scene_index: int = -1): # This method is called by GameWorld RPC to spawn enemies on clients @@ -158,6 +231,14 @@ func spawn_enemy_at_position(spawn_pos: Vector2, scene_index: int = -1): if "spawn_position" in enemy: enemy.spawn_position = spawn_pos + # 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) + + # Set multiplayer authority BEFORE adding to scene tree (CRITICAL!) + if multiplayer.has_multiplayer_peer(): + enemy.set_multiplayer_authority(1) + # Add to YSort node for automatic Y-sorting var ysort = get_parent().get_node_or_null("Entities") var parent = ysort if ysort else get_parent() @@ -167,9 +248,9 @@ func spawn_enemy_at_position(spawn_pos: Vector2, scene_index: int = -1): parent.add_child(enemy) - # Set multiplayer authority to server (peer 1) - if multiplayer.has_multiplayer_peer(): - enemy.set_multiplayer_authority(1) + # CRITICAL: Re-verify collision_mask AFTER adding to scene (in case _ready() or scene file overrides it) + # Use call_deferred to ensure _ready() has completed first, then set the entire mask + call_deferred("_verify_enemy_collision_mask", enemy) print(" ✓ Client spawned enemy: ", enemy.name, " at ", spawn_pos) @@ -184,18 +265,50 @@ func get_spawned_enemy_positions() -> Array: enemy_data.append({"position": enemy.global_position, "scene_index": scene_index}) return enemy_data +func _verify_enemy_collision_mask(enemy: Node): + # Verify and correct enemy collision_mask after _ready() has completed + # This ensures enemies always collide with walls (layer 7 = bit 6 = 64), not layer 3 or 4 + if not is_instance_valid(enemy): + return + + var expected_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64) + if enemy.collision_mask != expected_mask: + print("EnemySpawner: WARNING - Enemy ", enemy.name, " collision_mask was ", enemy.collision_mask, ", expected ", expected_mask, "! Correcting...") + enemy.collision_mask = expected_mask + + # Double-check by setting individual layers to be absolutely sure + enemy.set_collision_mask_value(1, true) # Players + enemy.set_collision_mask_value(2, true) # Objects + enemy.set_collision_mask_value(7, true) # Walls (layer 7) + enemy.set_collision_mask_value(3, false) # Ensure layer 3 is NOT set + enemy.set_collision_mask_value(4, false) # Ensure layer 4 is NOT set + print("EnemySpawner: Corrected enemy ", enemy.name, " collision_mask to ", enemy.collision_mask) + func _spawn_smoke_puff(): - print(" _spawn_smoke_puff() called") + # Legacy function - use _spawn_smoke_puff_at_position instead + _spawn_smoke_puff_at_position(global_position) + +func _spawn_smoke_puff_at_position(puff_position: Vector2) -> Node: + print(" _spawn_smoke_puff_at_position() called at ", puff_position) print(" smoke_puff_scene: ", smoke_puff_scene) if smoke_puff_scene: print(" Instantiating smoke puff...") var puff = smoke_puff_scene.instantiate() if puff: - puff.global_position = global_position - get_parent().add_child(puff) - print(" ✓ Smoke puff spawned at ", global_position) + puff.global_position = puff_position + var parent = get_parent() + if parent: + parent.add_child(puff) + print(" ✓ Smoke puff spawned at ", puff_position) + return puff + else: + print(" ERROR: No parent node for smoke puff!") + puff.queue_free() + return null else: print(" ERROR: Failed to instantiate smoke puff") + return null else: print(" WARNING: No smoke puff scene loaded") + return null diff --git a/src/scripts/floating_text.gd b/src/scripts/floating_text.gd index 099ef5b..603729b 100644 --- a/src/scripts/floating_text.gd +++ b/src/scripts/floating_text.gd @@ -1,35 +1,99 @@ extends Node2D -# Floating text that rises and fades out +# Floating text and item graphic that rises and fades out @onready var label = $Label +@onready var item_sprite = $ItemSprite # Sprite2D for item graphic (optional) var text: String = "" var color: Color = Color.WHITE -var duration: float = 1.0 +var display_duration: float = 0.5 # How long to show text/graphic before fading +var fade_duration: float = 0.5 # How long fade out takes var rise_distance: float = 30.0 +var is_coin: bool = false # Track if this is a coin (to animate) -func setup(text_value: String, text_color: Color): +func setup(text_value: String, text_color: Color, show_time: float = 0.5, fade_time: float = 0.5, item_texture: Texture2D = null, sprite_hframes: int = 1, sprite_vframes: int = 1, sprite_frame: int = 0): text = text_value color = text_color + display_duration = show_time + fade_duration = fade_time if label: label.text = text label.modulate = color + label.modulate.a = 1.0 # Start fully visible + + # Setup item sprite if texture provided + if item_sprite and item_texture: + item_sprite.visible = true + item_sprite.texture = item_texture + item_sprite.hframes = sprite_hframes + item_sprite.vframes = sprite_vframes + item_sprite.frame = sprite_frame + item_sprite.modulate = Color.WHITE + item_sprite.modulate.a = 1.0 + # Position sprite above label (if label exists) or centered + if label: + item_sprite.position = Vector2(0, -24) # Above the text (sprite is ~16px tall) + else: + item_sprite.position = Vector2(0, 0) + + # Check if this is a coin (6 frames horizontal, 1 frame vertical) - animate it + if sprite_hframes == 6 and sprite_vframes == 1: + is_coin = true + else: + # Hide sprite if no texture provided + if item_sprite: + item_sprite.visible = false + is_coin = false func _ready(): - # Animate rising and fading - var tween = create_tween() - tween.set_parallel(true) + # Start coin animation if needed + if is_coin and item_sprite: + _animate_coin() - # Move upward + # Show text/graphic for display_duration, then fade out over fade_duration + # Wait for display duration (text/graphic stays visible) + await get_tree().create_timer(display_duration).timeout + + # Then fade out over fade_duration + var fade_tween = create_tween() + fade_tween.set_parallel(true) + + # Move upward while fading var start_pos = global_position var end_pos = start_pos + Vector2(0, -rise_distance) - tween.tween_property(self, "global_position", end_pos, duration) + fade_tween.tween_property(self, "global_position", end_pos, fade_duration) - # Fade out - tween.tween_property(label, "modulate:a", 0.0, duration) + # Fade out label + if label: + fade_tween.tween_property(label, "modulate:a", 0.0, fade_duration) - # Remove after animation - tween.tween_callback(queue_free).set_delay(duration) + # Fade out sprite + if item_sprite and item_sprite.visible: + fade_tween.tween_property(item_sprite, "modulate:a", 0.0, fade_duration) + + # Remove after fade animation completes + fade_tween.tween_callback(queue_free).set_delay(fade_duration) + +func _animate_coin(): + # Animate coin rotation during display (similar to loot coin animation) + if not is_coin or not item_sprite or not item_sprite.visible: + return + + # Use _process to animate coin frames continuously + # We'll animate at 10 frames per second during the display + var coin_anim_speed = 10.0 # Frames per second + var coin_frame_time = 1.0 / coin_anim_speed # Time per frame + + # Create a tween that cycles through frames + var total_time = display_duration + fade_duration # Total display time + var frames_to_cycle = int(total_time * coin_anim_speed) + + # Animate coin frames + for i in range(frames_to_cycle): + var target_frame = i % item_sprite.hframes # Cycle through 0-5 + await get_tree().create_timer(coin_frame_time).timeout + if item_sprite and is_instance_valid(item_sprite) and item_sprite.visible: + item_sprite.frame = target_frame diff --git a/src/scripts/floor_switch.gd b/src/scripts/floor_switch.gd new file mode 100644 index 0000000..5adddc2 --- /dev/null +++ b/src/scripts/floor_switch.gd @@ -0,0 +1,256 @@ +extends Area2D + +# Floor Switch - Activates when enough weight is placed on it + +@export_enum("walk", "pillar") var switch_type: String = "walk" # "walk" = walk-on switch (weight 1), "pillar" = requires pillar (weight 5) +@export var required_weight: float = 1.0 # Required weight to activate (automatically set based on switch_type) + +var is_activated: bool = false +var current_weight: float = 0.0 +var objects_on_switch: Array = [] # Track objects currently on the switch + +var tilemap_layer: TileMapLayer = null +var switch_tile_position: Vector2i = Vector2i.ZERO # Tile position in the tilemap +var check_timer: float = 0.0 # Timer for periodic checks (pillar switches only) +var check_interval: float = 0.2 # Check every 0.2 seconds + +func _ready(): + # Set required weight based on switch type + if switch_type == "walk": + required_weight = 1.0 # Player weight only + elif switch_type == "pillar": + required_weight = 5.0 # Requires pillar (weight 5) + + # Set collision mask to detect players and objects + collision_layer = 0 + collision_mask = 1 | 2 # Detect players (layer 1) and objects (layer 2) + + # Connect signals + body_entered.connect(_on_body_entered) + body_exited.connect(_on_body_exited) + + # Find tilemap layer to update switch visual (deferred to ensure game_world is ready) + call_deferred("_find_tilemap_layer") + call_deferred("_update_visual") + + # For pillar switches, enable _process to periodically check if pillars are still not being held + # This handles the case where a player picks up a pillar that's already on the switch + # Walk switches don't need this check, so _process is disabled by default + if switch_type == "pillar": + set_process(true) + else: + set_process(false) + +func _find_tilemap_layer(): + # Find tilemap layer to update switch visual + var game_world = get_tree().get_first_node_in_group("game_world") + if game_world: + if "dungeon_tilemap_layer" in game_world: + tilemap_layer = game_world.dungeon_tilemap_layer + else: + # Try to find it in Environment node + var environment = game_world.get_node_or_null("Environment") + if environment: + tilemap_layer = environment.get_node_or_null("DungeonLayer0") + +func _on_body_entered(body): + # Object entered the switch + # CRITICAL: For pillar switches, only count pillars (not being held), ignore players + # For walk switches, count players and any objects they're carrying + if switch_type == "pillar": + # Pillar switch: Only count pillars that are NOT being held + if body.has_method("can_be_grabbed") and body.can_be_grabbed(): + # Check if object is a pillar and not being held + 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 + + # Only count pillars that are placed (not being held) + 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 + objects_on_switch.append(body) + current_weight += weight + print("FloorSwitch: Pillar entered switch (not held), weight: ", weight, ", total: ", current_weight) + # Enable _process to periodically check if pillar is picked up + if not is_processing(): + set_process(true) + _check_activation() + # Ignore players completely for pillar switches + else: + # Walk switch: Count players and any objects they're carrying + 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: + objects_on_switch.append(body) + current_weight += weight + _check_activation() + +func _on_body_exited(body): + # Object left the switch + if body in objects_on_switch: + # 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 + + # 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: + objects_on_switch.erase(body) + current_weight -= weight + print("FloorSwitch: Pillar left switch, weight: ", weight, ", total: ", current_weight) + _check_activation() + else: + # Walk switch: Remove any object + var weight = _get_object_weight(body) + if weight > 0: + objects_on_switch.erase(body) + current_weight -= weight + _check_activation() + +func _get_object_weight(body: Node) -> float: + # Get weight of an object + # CRITICAL: For pillar switches, this function should NOT be called for players + # (they are filtered out in _on_body_entered) + if body.is_in_group("player"): + # Player base weight = 1 + var weight = 1.0 + # If player is carrying another player or object, add their weight + # held_object is a property in player.gd, so access directly + # NOTE: For pillar switches, players should never activate, so held objects don't matter + if body.held_object: + if body.held_object.is_in_group("player"): + weight += 1.0 + else: + # Check if held object has weight property (for interactable objects) + # But only for walk switches - pillar switches don't count held objects + if switch_type == "walk": + var held_weight = _get_object_weight(body.held_object) + if held_weight > 0: + weight += held_weight + return weight + else: + # For interactable objects, weight is an exported variable + # Try to access it directly - if it doesn't exist, will return 0 + var weight_value = body.get("weight") + if weight_value != null: + return weight_value as float + # Fallback: check if object has get_weight method + if body.has_method("get_weight"): + return body.get_weight() + return 0.0 + +func _check_activation(): + # CRITICAL: For pillar switches, verify that objects on switch are NOT being held + # If any pillar on switch is now being held, remove it from weight calculation + if switch_type == "pillar": + var valid_objects = [] + var new_weight = 0.0 + + for obj in objects_on_switch: + if not is_instance_valid(obj): + continue + + var object_type = obj.get("object_type") if "object_type" in obj else "" + var is_being_held = obj.get("is_being_held") if "is_being_held" in obj else false + + # Only count pillars that are NOT being held + if object_type == "Pillar" and not is_being_held: + var weight = _get_object_weight(obj) + if weight >= required_weight: + valid_objects.append(obj) + new_weight += weight + else: + # Object is being held or not a pillar - remove it + print("FloorSwitch: Removing held/invalid object from switch: ", obj.name if obj else "null") + + # Update objects list and weight + objects_on_switch = valid_objects + current_weight = new_weight + + # Check if switch should be activated + var should_activate = current_weight >= required_weight + + if should_activate != is_activated: + is_activated = should_activate + _update_visual() + + # Notify connected doors + _notify_doors() + +func _update_visual(): + # Update tile visual to show activated/inactive state + if not tilemap_layer: + return + + # Choose tiles based on switch type + var tile_coord: Vector2i + if switch_type == "walk": + # Walk-on switch: 11,9 (inactive) and 12,9 (active) + tile_coord = Vector2i(12, 9) if is_activated else Vector2i(11, 9) + elif switch_type == "pillar": + # Pillar switch: 16,9 (inactive) and 17,9 (active) + tile_coord = Vector2i(17, 9) if is_activated else Vector2i(16, 9) + else: + # Fallback to walk-on switch + tile_coord = Vector2i(12, 9) if is_activated else Vector2i(11, 9) + + tilemap_layer.set_cell(switch_tile_position, 0, tile_coord) + +func _process(delta): + # For pillar switches, periodically check if pillars on switch are still not being held + # This handles the case where a player picks up a pillar that's already on the switch + # Only check if there are objects on the switch (optimization) + if objects_on_switch.size() > 0: + check_timer += delta + if check_timer >= check_interval: + check_timer = 0.0 + _check_activation() + else: + # No objects on switch - disable _process to save performance + # It will be re-enabled when an object enters the switch + set_process(false) + +func _notify_doors(): + # Notify only doors that are explicitly connected to this switch + # CRITICAL: Only notify doors that have this switch in their connected_switches list + # Do NOT use position-based checks - that causes cross-room door triggering! + var game_world = get_tree().get_first_node_in_group("game_world") + if not game_world: + return + + var entities_node = game_world.get_node_or_null("Entities") + if not entities_node: + return + + # Get this switch's room for verification + var switch_room = get_meta("switch_room") if has_meta("switch_room") else {} + + # Find all blocking doors and check if they are connected to this switch + for child in entities_node.get_children(): + if child.is_in_group("blocking_door") or child.name.begins_with("BlockingDoor_"): + if not is_instance_valid(child): + continue + + # CRITICAL SAFETY CHECK: Verify door is in the same room as this switch + # Only notify doors that are in the SAME room as the switch + var door_blocking_room = child.blocking_room if "blocking_room" in child else {} + var rooms_match = false + if switch_room and not switch_room.is_empty() and door_blocking_room and not door_blocking_room.is_empty(): + rooms_match = (switch_room.x == door_blocking_room.x and \ + switch_room.y == door_blocking_room.y and \ + switch_room.w == door_blocking_room.w and \ + switch_room.h == door_blocking_room.h) + + # CRITICAL: Only notify doors that have this switch in their connected_switches list + # AND verify rooms match (double-check to prevent cross-room triggering) + if child.connected_switches.has(self): + if rooms_match: + # This switch is explicitly connected to this door AND in the same room - trigger puzzle check + if child.has_method("_check_puzzle_state"): + var door_room_info = "room:(" + str(door_blocking_room.get("x", "?")) + "," + str(door_blocking_room.get("y", "?")) + ")" if door_blocking_room and not door_blocking_room.is_empty() else "no room" + print("FloorSwitch: Notifying door ", child.name, " (", door_room_info, ") that switch ", name, " (room: ", switch_room.get("x", "?") if switch_room and not switch_room.is_empty() else "?", ",", switch_room.get("y", "?") if switch_room and not switch_room.is_empty() else "?", ") state changed to ", is_activated) + child._check_puzzle_state() + else: + push_error("FloorSwitch: ERROR - Switch ", name, " is connected to door ", child.name, " but rooms don't match! Switch room: (", switch_room.get("x", "?") if switch_room and not switch_room.is_empty() else "?", ",", switch_room.get("y", "?") if switch_room and not switch_room.is_empty() else "?", "), Door room: (", door_blocking_room.get("x", "?") if door_blocking_room and not door_blocking_room.is_empty() else "?", ",", door_blocking_room.get("y", "?") if door_blocking_room and not door_blocking_room.is_empty() else "?", ") - NOT notifying!") diff --git a/src/scripts/floor_switch.gd.uid b/src/scripts/floor_switch.gd.uid new file mode 100644 index 0000000..d42b7f1 --- /dev/null +++ b/src/scripts/floor_switch.gd.uid @@ -0,0 +1 @@ +uid://dfigqc0flmid5 diff --git a/src/scripts/game_world.gd b/src/scripts/game_world.gd index 88cc566..2ca97a3 100644 --- a/src/scripts/game_world.gd +++ b/src/scripts/game_world.gd @@ -22,7 +22,7 @@ var level_exp_collected: float = 0.0 var level_coins_collected: int = 0 # Client ready tracking (server only) -var clients_ready: Dictionary = {} # peer_id -> bool +var clients_ready: Dictionary = {} # peer_id -> bool func _ready(): # Add to group for easy access @@ -222,7 +222,7 @@ func _sync_enemy_position(enemy_name: String, enemy_index: int, pos: Vector2, ve # Clients receive enemy position updates from server # Find the enemy by name or index if multiplayer.is_server(): - return # Server ignores this (it's the sender) + return # Server ignores this (it's the sender) var entities_node = get_node_or_null("Entities") if not entities_node: @@ -248,7 +248,7 @@ func _sync_enemy_death(enemy_name: String, enemy_index: int): # Clients receive enemy death sync from server # Find the enemy by name or index if multiplayer.is_server(): - return # Server ignores this (it's the sender) + return # Server ignores this (it's the sender) var entities_node = get_node_or_null("Entities") if not entities_node: @@ -278,7 +278,7 @@ func _sync_enemy_damage_visual(enemy_name: String, enemy_index: int): # Clients receive enemy damage visual sync from server # Find the enemy by name or index if multiplayer.is_server(): - return # Server ignores this (it's the sender) + return # Server ignores this (it's the sender) var entities_node = get_node_or_null("Entities") if not entities_node: @@ -337,7 +337,7 @@ func _request_loot_pickup(loot_id: int, loot_position: Vector2, player_peer_id: func _sync_show_level_complete(enemies_defeated: int, times_downed: int, exp_collected: float, coins_collected: int): # Clients receive level complete UI sync from server if multiplayer.is_server(): - return # Server ignores this (it's the sender) + return # Server ignores this (it's the sender) # Update stats before showing level_enemies_defeated = enemies_defeated @@ -352,7 +352,7 @@ func _sync_show_level_complete(enemies_defeated: int, times_downed: int, exp_col func _sync_hide_level_complete(): # Clients receive hide level complete UI sync from server if multiplayer.is_server(): - return # Server ignores this (it's the sender) + return # Server ignores this (it's the sender) var level_complete_ui = get_node_or_null("LevelCompleteUI") if level_complete_ui: @@ -362,7 +362,7 @@ func _sync_hide_level_complete(): func _sync_show_level_number(level: int): # Clients receive level number UI sync from server if multiplayer.is_server(): - return # Server ignores this (it's the sender) + return # Server ignores this (it's the sender) current_level = level _show_level_number() @@ -372,7 +372,7 @@ func _sync_loot_remove(loot_id: int, loot_position: Vector2): # Clients receive loot removal sync from server # Find the loot by ID or position if multiplayer.is_server(): - return # Server ignores this (it's the sender) + return # Server ignores this (it's the sender) var entities_node = get_node_or_null("Entities") if not entities_node: @@ -469,6 +469,12 @@ func _generate_dungeon(): # Spawn interactable objects _spawn_interactable_objects() + # Spawn blocking doors + _spawn_blocking_doors() + + # Spawn room triggers + _spawn_room_triggers() + # Wait a frame to ensure enemies and objects are properly in scene tree before syncing await get_tree().process_frame @@ -483,7 +489,7 @@ func _generate_dungeon(): _move_all_players_to_start_room() # Update camera immediately to ensure it's looking at the players - await get_tree().process_frame # Wait a frame for players to be fully in scene tree + await get_tree().process_frame # Wait a frame for players to be fully in scene tree _update_camera() # Show level number (for initial level generation only - not when called from level completion) @@ -815,7 +821,7 @@ func _is_safe_spawn_position(world_pos: Vector2) -> bool: return false # Check if it's a floor tile - if grid[tile_x][tile_y] == 1: # Floor + if grid[tile_x][tile_y] == 1: # Floor return true return false @@ -824,14 +830,13 @@ func _find_nearby_safe_spawn_position(world_pos: Vector2, max_distance: float = # Find a nearby safe spawn position (on a floor tile) # Returns the original position if it's safe, otherwise finds the nearest safe position # max_distance: Maximum distance to search for a safe position - # First check if the original position is safe if _is_safe_spawn_position(world_pos): return world_pos # Search in expanding circles around the position var tile_size = 16 - var search_radius = 1 # Start with 1 tile radius + var search_radius = 1 # Start with 1 tile radius var max_radius = int(max_distance / tile_size) + 1 while search_radius <= max_radius: @@ -898,7 +903,7 @@ func _sync_dungeon(dungeon_data_sync: Dictionary, seed_value: int, level: int, h dungeon_data = dungeon_data_sync dungeon_seed = seed_value - current_level = level # Update current_level FIRST before showing level number + current_level = level # Update current_level FIRST before showing level number print("GameWorld: Client updated current_level to ", current_level, " from sync") # Clear previous level on client @@ -906,7 +911,7 @@ func _sync_dungeon(dungeon_data_sync: Dictionary, seed_value: int, level: int, h # Wait for old entities to be fully freed before spawning new ones await get_tree().process_frame - await get_tree().process_frame # Wait an extra frame to ensure cleanup is complete + await get_tree().process_frame # Wait an extra frame to ensure cleanup is complete # Render dungeon on client _render_dungeon() @@ -920,9 +925,15 @@ func _sync_dungeon(dungeon_data_sync: Dictionary, seed_value: int, level: int, h # Spawn interactable objects on client _spawn_interactable_objects() + # Spawn blocking doors on client + _spawn_blocking_doors() + + # Spawn room triggers on client + _spawn_room_triggers() + # Wait a frame to ensure all enemies and objects are properly added to scene tree and initialized await get_tree().process_frame - await get_tree().process_frame # Wait an extra frame to ensure enemies are fully ready + await get_tree().process_frame # Wait an extra frame to ensure enemies are fully ready # Update spawn points - use host's room if available, otherwise use start room if not host_room.is_empty(): @@ -1072,10 +1083,20 @@ func _spawn_enemies(): if "damage" in enemy_data: enemy.damage = enemy_data.damage + # 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) + # Add to scene tree AFTER setting authority and stats entities_node.add_child(enemy) enemy.global_position = enemy_data.position + # CRITICAL: Re-verify collision_mask AFTER adding to scene (in case scene file overrides it) + # This ensures enemies always collide with walls (layer 7 = bit 6 = 64) + if enemy.collision_mask != (1 | 2 | 64): + print("GameWorld: WARNING - Enemy ", enemy.name, " collision_mask was ", enemy.collision_mask, ", expected ", (1 | 2 | 64), "! Correcting...") + enemy.collision_mask = 1 | 2 | 64 + # Verify authority is still set after adding to tree if multiplayer.has_multiplayer_peer(): var auth_after = enemy.get_multiplayer_authority() @@ -1228,7 +1249,7 @@ func _sync_dungeon_enemy_spawn(enemy_data: Dictionary): if child.is_in_group("enemy") and child.has_meta("dungeon_spawned"): # Check if it's a duplicate by position var child_pos = child.global_position - if child_pos.distance_to(enemy_data.position) < 1.0: # Same position + if child_pos.distance_to(enemy_data.position) < 1.0: # Same position # Also check if it's dead - if so, remove it first if "is_dead" in child and child.is_dead: print("GameWorld: Removing dead duplicate enemy at ", enemy_data.position) @@ -1275,10 +1296,20 @@ func _sync_dungeon_enemy_spawn(enemy_data: Dictionary): if "damage" in enemy_data: enemy.damage = enemy_data.damage + # 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) + # Add to scene tree AFTER setting authority and stats entities_node.add_child(enemy) enemy.global_position = enemy_data.position + # CRITICAL: Re-verify collision_mask AFTER adding to scene (in case scene file overrides it) + # This ensures enemies always collide with walls (layer 7 = bit 6 = 64) + if enemy.collision_mask != (1 | 2 | 64): + print("GameWorld: WARNING - Enemy ", enemy.name, " collision_mask was ", enemy.collision_mask, ", expected ", (1 | 2 | 64), "! Correcting...") + enemy.collision_mask = 1 | 2 | 64 + # Verify authority is still set if multiplayer.has_multiplayer_peer(): var auth_after = enemy.get_multiplayer_authority() @@ -1338,12 +1369,12 @@ func _clear_level(): # Free all entities immediately (not queue_free) to ensure they're gone before spawning new ones for entity in entities_to_remove: if is_instance_valid(entity): - entity.free() # Use free() instead of queue_free() for immediate removal + entity.free() # Use free() instead of queue_free() for immediate removal # Remove stairs area var stairs_area = get_node_or_null("StairsArea") if stairs_area: - stairs_area.free() # Use free() for immediate removal + stairs_area.free() # Use free() for immediate removal # Clear dungeon data (but keep it for now until new one is generated) # dungeon_data = {} # Don't clear yet, wait for new generation @@ -1394,7 +1425,7 @@ func _create_stairs_area(): # Set collision layer/mask BEFORE adding to scene stairs_area.collision_layer = 0 - stairs_area.collision_mask = 1 # Detect players (layer 1) + stairs_area.collision_mask = 1 # Detect players (layer 1) # Add script BEFORE adding to scene (so _ready() is called properly) var stairs_script = load("res://scripts/stairs.gd") @@ -1419,7 +1450,7 @@ func _create_stairs_area(): func _on_player_reached_stairs(player: Node): # Player reached stairs - trigger level complete if not multiplayer.is_server() and multiplayer.has_multiplayer_peer(): - return # Only server handles this + return # Only server handles this print("GameWorld: Player ", player.name, " reached stairs!") @@ -1443,7 +1474,7 @@ func _on_player_reached_stairs(player: Node): _sync_show_level_complete.rpc(level_enemies_defeated, level_times_downed, level_exp_collected, level_coins_collected) # After delay, hide UI and generate new level - await get_tree().create_timer(5.0).timeout # Show stats for 5 seconds + await get_tree().create_timer(5.0).timeout # Show stats for 5 seconds # Hide level complete UI (server and clients) var level_complete_ui = get_node_or_null("LevelCompleteUI") @@ -1458,7 +1489,7 @@ func _on_player_reached_stairs(player: Node): # Wait for old entities to be fully freed before generating new level await get_tree().process_frame - await get_tree().process_frame # Wait an extra frame to ensure cleanup is complete + await get_tree().process_frame # Wait an extra frame to ensure cleanup is complete # Generate next level current_level += 1 @@ -1471,7 +1502,7 @@ func _on_player_reached_stairs(player: Node): # We need to wait for all the async operations in _generate_dungeon() to finish await get_tree().process_frame await get_tree().process_frame - await get_tree().process_frame # Extra frame to ensure everything is done + await get_tree().process_frame # Extra frame to ensure everything is done # Verify current_level is still correct print("GameWorld: After dungeon generation, current_level = ", current_level) @@ -1493,7 +1524,7 @@ func _on_player_reached_stairs(player: Node): # Sync new level to all clients - use start room since all players should be there # IMPORTANT: Wait multiple frames to ensure dungeon generation and enemy spawning is complete before syncing await get_tree().process_frame - await get_tree().process_frame # Wait extra frames to ensure enemies are fully initialized + await get_tree().process_frame # Wait extra frames to ensure enemies are fully initialized if multiplayer.has_multiplayer_peer(): var start_room = dungeon_data.start_room if not dungeon_data.is_empty() and dungeon_data.has("start_room") else {} @@ -1603,21 +1634,27 @@ func _fade_in_player(player: Node): for sprite_layer in sprite_layers: if sprite_layer: - sprite_layer.modulate.a = 0.0 # Start invisible + sprite_layer.modulate.a = 0.0 # Start invisible fade_tween.tween_property(sprite_layer, "modulate:a", 1.0, 1.0) func _show_level_complete_ui(): # Create or show level complete UI var level_complete_ui = get_node_or_null("LevelCompleteUI") if not level_complete_ui: - # Try to load scene, but fall back to programmatic creation if it doesn't exist - var level_complete_scene = load("res://scenes/level_complete_ui.tscn") - if level_complete_scene: - level_complete_ui = level_complete_scene.instantiate() - level_complete_ui.name = "LevelCompleteUI" - add_child(level_complete_ui) + # Try to load scene if it exists, but fall back to programmatic creation if it doesn't + var scene_path = "res://scenes/level_complete_ui.tscn" + if ResourceLoader.exists(scene_path): + var level_complete_scene = load(scene_path) + if level_complete_scene: + level_complete_ui = level_complete_scene.instantiate() + level_complete_ui.name = "LevelCompleteUI" + add_child(level_complete_ui) + else: + # Scene file exists but failed to load - fall back to programmatic creation + print("GameWorld: Warning - level_complete_ui.tscn exists but failed to load, creating programmatically") + level_complete_ui = _create_level_complete_ui_programmatically() else: - # Create UI programmatically if scene doesn't exist + # Scene file doesn't exist - create UI programmatically (expected behavior) level_complete_ui = _create_level_complete_ui_programmatically() if level_complete_ui: @@ -1634,14 +1671,20 @@ func _show_level_number(): print("GameWorld: _show_level_number() called with current_level = ", current_level) var level_text_ui = get_node_or_null("LevelTextUI") if not level_text_ui: - # Try to load scene, but fall back to programmatic creation if it doesn't exist - var level_text_scene = load("res://scenes/level_text_ui.tscn") - if level_text_scene: - level_text_ui = level_text_scene.instantiate() - level_text_ui.name = "LevelTextUI" - add_child(level_text_ui) + # Try to load scene if it exists, but fall back to programmatic creation if it doesn't + var scene_path = "res://scenes/level_text_ui.tscn" + if ResourceLoader.exists(scene_path): + var level_text_scene = load(scene_path) + if level_text_scene: + level_text_ui = level_text_scene.instantiate() + level_text_ui.name = "LevelTextUI" + add_child(level_text_ui) + else: + # Scene file exists but failed to load - fall back to programmatic creation + print("GameWorld: Warning - level_text_ui.tscn exists but failed to load, creating programmatically") + level_text_ui = _create_level_text_ui_programmatically() else: - # Create UI programmatically if scene doesn't exist + # Scene file doesn't exist - create UI programmatically (expected behavior) level_text_ui = _create_level_text_ui_programmatically() if level_text_ui: @@ -1664,7 +1707,7 @@ func _create_level_complete_ui_programmatically() -> Node: var vbox = VBoxContainer.new() vbox.set_anchors_preset(Control.PRESET_CENTER) - vbox.offset_top = -200 # Position a bit up from center + vbox.offset_top = -200 # Position a bit up from center canvas_layer.add_child(vbox) # Title @@ -1757,3 +1800,649 @@ func _move_players_to_host_room(host_room: Dictionary): player.position = new_pos print("GameWorld: Moved player ", player.name, " to ", new_pos) spawn_index += 1 + +func _spawn_blocking_doors(): + # Spawn blocking doors from dungeon data + if dungeon_data.is_empty() or not dungeon_data.has("blocking_doors"): + return + + var blocking_doors = dungeon_data.blocking_doors + if blocking_doors == null or not blocking_doors is Array: + return + + var door_scene = preload("res://scenes/door.tscn") + if not door_scene: + push_error("ERROR: Could not load door scene!") + return + + var entities_node = get_node_or_null("Entities") + if not entities_node: + push_error("ERROR: Could not find Entities node!") + return + + print("GameWorld: Spawning ", blocking_doors.size(), " blocking doors") + + for i in range(blocking_doors.size()): + var door_data = blocking_doors[i] + if not door_data is Dictionary: + continue + + var door = door_scene.instantiate() + door.name = "BlockingDoor_%d" % i + door.add_to_group("blocking_door") + + # Set door properties BEFORE adding to scene (so _ready() has correct values) + door.type = door_data.type if "type" in door_data else "StoneDoor" + door.direction = door_data.direction if "direction" in door_data else "Up" + door.is_closed = door_data.is_closed if "is_closed" in door_data else true + + # CRITICAL: Set puzzle requirements based on door_data + if "puzzle_type" in door_data: + if door_data.puzzle_type == "enemy": + door.requires_enemies = true + door.requires_switch = false + print("GameWorld: Door ", door.name, " requires enemies to open (puzzle_type: enemy)") + elif door_data.puzzle_type in ["switch_walk", "switch_pillar"]: + door.requires_enemies = false + door.requires_switch = true + print("GameWorld: Door ", door.name, " requires switch to open (puzzle_type: ", door_data.puzzle_type, ")") + door.blocking_room = door_data.blocking_room if "blocking_room" in door_data else {} + door.switch_room = door_data.switch_room if "switch_room" in door_data else {} + + # CRITICAL: Verify door has blocking_room set - StoneDoor/GateDoor MUST be in a puzzle room + if (door_data.type == "StoneDoor" or door_data.type == "GateDoor"): + if not "blocking_room" in door_data or door_data.blocking_room.is_empty(): + push_error("GameWorld: ERROR - Blocking door ", door.name, " (", door_data.type, ") has no blocking_room! This door should not exist! Removing it.") + door.queue_free() + continue + + # CRITICAL: Verify door has puzzle_type - StoneDoor/GateDoor MUST have a puzzle + if not "puzzle_type" in door_data: + push_error("GameWorld: ERROR - Blocking door ", door.name, " (", door_data.type, ") has no puzzle_type! This door should not exist! Removing it.") + door.queue_free() + continue + + 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 + # 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: + var original_door = door_data.door + if "room1" in original_door and original_door.room1: + door.room1 = original_door.room1 + if "room2" in original_door and original_door.room2: + door.room2 = original_door.room2 + + # CRITICAL: For StoneDoor/GateDoor, verify door.room1 matches blocking_room + # The door should be IN the puzzle room (room1 == blocking_room) + if (door_data.type == "StoneDoor" or door_data.type == "GateDoor") and door.blocking_room and not door.blocking_room.is_empty(): + if not door.room1 or door.room1.is_empty(): + push_error("GameWorld: ERROR - Blocking door ", door.name, " has no room1! Cannot verify it's in puzzle room! Removing it.") + door.queue_free() + continue + + # Verify room1 (where door is) matches blocking_room (puzzle room) + var room1_matches_blocking = (door.room1.x == door.blocking_room.x and \ + door.room1.y == door.blocking_room.y and \ + door.room1.w == door.blocking_room.w and \ + door.room1.h == door.blocking_room.h) + + if not room1_matches_blocking: + push_error("GameWorld: ERROR - Blocking door ", door.name, " room1 (", door.room1.x, ",", door.room1.y, ") doesn't match blocking_room (", door.blocking_room.x, ",", door.blocking_room.y, ")! This door is NOT in the puzzle room! Removing it.") + door.queue_free() + continue + + print("GameWorld: Blocking door ", door.name, " verified - room1 (", door.room1.x, ",", door.room1.y, ") == blocking_room (", door.blocking_room.x, ",", door.blocking_room.y, ") - door is IN puzzle room") + + # Set multiplayer authority BEFORE adding to scene + if multiplayer.has_multiplayer_peer(): + door.set_multiplayer_authority(1) + + # CRITICAL: Set position BEFORE adding to scene tree (so _ready() can use it) + door.global_position = door_data.position if "position" in door_data else Vector2.ZERO + + # Add to scene (this triggers _ready() which will use the position we just set) + entities_node.add_child(door) + + # NOTE: Doors are connected to room triggers automatically by room_trigger._find_room_entities() + # No need to manually connect them here + + # CRITICAL SAFETY CHECK: Verify door is for a puzzle room (StoneDoor/GateDoor should ONLY exist in puzzle rooms) + if door_data.type == "StoneDoor" or door_data.type == "GateDoor": + if not "blocking_room" in door_data or door_data.blocking_room.is_empty(): + push_error("GameWorld: ERROR - Blocking door ", door.name, " (", door_data.type, ") has no blocking_room! This door should not exist!") + door.queue_free() + continue + + if not "puzzle_type" in door_data: + push_error("GameWorld: ERROR - Blocking door ", door.name, " (", door_data.type, ") has no puzzle_type! This door should not exist!") + door.queue_free() + continue + + # CRITICAL: Verify that this door actually has puzzle elements + # Puzzle elements should already be created in dungeon_generator, but verify they exist + var has_puzzle_element = false + + # Spawn floor switch if this door requires one (puzzle_type is "switch_walk" or "switch_pillar") + if "puzzle_type" in door_data and (door_data.puzzle_type == "switch_walk" or door_data.puzzle_type == "switch_pillar"): + if "floor_switch_position" in door_data or ("switch_data" in door_data and door_data.switch_data.has("position")): + var switch_pos = door_data.floor_switch_position if "floor_switch_position" in door_data else door_data.switch_data.position + var switch_tile_x = door_data.switch_tile_x if "switch_tile_x" in door_data else door_data.switch_data.tile_x + var switch_tile_y = door_data.switch_tile_y if "switch_tile_y" in door_data else door_data.switch_data.tile_y + var switch_type = door_data.switch_type if "switch_type" in door_data else ("walk" if door_data.puzzle_type == "switch_walk" else "pillar") + var switch_weight = door_data.switch_required_weight if "switch_required_weight" in door_data else (1.0 if switch_type == "walk" else 5.0) + + # CRITICAL: Check if switch already exists for THIS SPECIFIC ROOM (to avoid duplicates) + # Only connect to switches in the SAME blocking_room - never connect across rooms! + var existing_switch = null + var door_blocking_room = door_data.blocking_room if "blocking_room" in door_data else {} + + # CRITICAL: Verify door has valid blocking_room before searching for switches + if door_blocking_room.is_empty(): + push_error("GameWorld: ERROR - Door ", door.name, " has empty blocking_room! Cannot find switches!") + continue + + for existing in get_tree().get_nodes_in_group("floor_switch"): + if not is_instance_valid(existing): + continue + + # CRITICAL: Check ROOM FIRST (most important), then position + # Switches MUST have switch_room metadata set when spawned + if not existing.has_meta("switch_room"): + continue # Switch has no room metadata - skip it (can't verify it's in the right room) + + var existing_switch_room = existing.get_meta("switch_room") + if existing_switch_room.is_empty(): + continue # Invalid room data + + # CRITICAL: Verify switch is in the SAME room as door (check room FIRST) + var room_match = (existing_switch_room.x == door_blocking_room.x and \ + existing_switch_room.y == door_blocking_room.y and \ + existing_switch_room.w == door_blocking_room.w and \ + existing_switch_room.h == door_blocking_room.h) + + if not room_match: + # Switch is in a different room - DO NOT connect, skip it + continue + + # Room matches - now check position (must be exact match) + var pos_match = existing.global_position.distance_to(switch_pos) < 1.0 + if pos_match: + # Both room AND position match - this is the correct switch + existing_switch = existing + print("GameWorld: Found existing switch ", existing.name, " in room (", door_blocking_room.x, ",", door_blocking_room.y, ") at position ", existing.global_position, " matching door room and position") + break + + if existing_switch: + # CRITICAL: Double-check room match before connecting + var existing_switch_room_final = existing_switch.get_meta("switch_room") + var final_room_match = false + if existing_switch_room_final and not existing_switch_room_final.is_empty() and door_blocking_room and not door_blocking_room.is_empty(): + final_room_match = (existing_switch_room_final.x == door_blocking_room.x and \ + existing_switch_room_final.y == door_blocking_room.y and \ + existing_switch_room_final.w == door_blocking_room.w and \ + existing_switch_room_final.h == door_blocking_room.h) + + if final_room_match: + # Switch already exists in the SAME room - connect door to existing switch + 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") + 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 + existing_switch = null + else: + # Spawn new switch - CRITICAL: Only spawn if we have valid room data + if not door_blocking_room or door_blocking_room.is_empty(): + push_error("GameWorld: ERROR - Cannot spawn switch for door ", door.name, " - no blocking_room!") + continue + + # CRITICAL: Verify switch position matches door_data switch position exactly + # If switch_room in door_data doesn't match blocking_room, it's an error + if "switch_room" in door_data: + var door_switch_room = door_data.switch_room + if door_switch_room and not door_switch_room.is_empty(): + var switch_room_matches = (door_switch_room.x == door_blocking_room.x and \ + door_switch_room.y == door_blocking_room.y and \ + door_switch_room.w == door_blocking_room.w and \ + door_switch_room.h == door_blocking_room.h) + if not switch_room_matches: + push_error("GameWorld: ERROR - Door ", door.name, " switch_room (", door_switch_room.x, ",", door_switch_room.y, ") doesn't match blocking_room (", door_blocking_room.x, ",", door_blocking_room.y, ")! This is a bug!") + door.queue_free() + continue + + var switch = _spawn_floor_switch(switch_pos, switch_weight, switch_tile_x, switch_tile_y, switch_type, door_blocking_room) + if switch: + # CRITICAL: Verify switch has room metadata set (should be set in _spawn_floor_switch) + if not switch.has_meta("switch_room"): + push_error("GameWorld: ERROR - Switch ", switch.name, " was spawned without switch_room metadata! Setting it now as fallback.") + switch.set_meta("switch_room", door_blocking_room) # Set it now as fallback + + # CRITICAL: Verify switch room matches door blocking_room before connecting + # This ensures switches are ONLY connected to doors in the SAME room + var switch_room_check = switch.get_meta("switch_room") + if switch_room_check and not switch_room_check.is_empty() and door_blocking_room and not door_blocking_room.is_empty(): + var room_match_before_connect = (switch_room_check.x == door_blocking_room.x and \ + switch_room_check.y == door_blocking_room.y and \ + switch_room_check.w == door_blocking_room.w and \ + switch_room_check.h == door_blocking_room.h) + + if room_match_before_connect: + # Connect switch to door ONLY if rooms match exactly + door.connected_switches.append(switch) + has_puzzle_element = true + print("GameWorld: Spawned switch ", switch.name, " in room (", door_blocking_room.x, ",", door_blocking_room.y, ") and connected to door ", door.name, " in SAME room") + + # If this is a pillar switch, place a pillar in the same room + if switch_type == "pillar": + _place_pillar_in_room(door_blocking_room, switch_pos) + 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 + has_puzzle_element = false # Don't count this as puzzle element + else: + push_error("GameWorld: ERROR - Switch ", switch.name, " or door ", door.name, " has invalid room data! Switch room: ", switch_room_check, ", Door room: ", door_blocking_room) + switch.queue_free() # Remove invalid switch + has_puzzle_element = false + else: + push_warning("GameWorld: WARNING - Failed to spawn floor switch for door ", door.name, "!") + + # Place key in room if this is a KeyDoor + if door_data.type == "KeyDoor" and "key_room" in door_data: + _place_key_in_room(door_data.key_room) + has_puzzle_element = true # KeyDoors are always valid + + # Spawn enemy spawners if this door requires enemies (puzzle_type is "enemy") + if "puzzle_type" in door_data and door_data.puzzle_type == "enemy": + print("GameWorld: ===== Door ", door.name, " has puzzle_type 'enemy' - checking for enemy_spawners =====") + if "enemy_spawners" in door_data and door_data.enemy_spawners is Array: + print("GameWorld: Door has enemy_spawners array with ", door_data.enemy_spawners.size(), " spawners") + var spawner_created = false + for spawner_data in door_data.enemy_spawners: + if spawner_data is Dictionary and spawner_data.has("position"): + # Check if spawner already exists for this room (to avoid duplicates) + var existing_spawner = null + for existing in get_tree().get_nodes_in_group("enemy_spawner"): + if existing.global_position.distance_to(spawner_data.position) < 1.0: + existing_spawner = existing + break + + if existing_spawner: + # Spawner already exists - just verify it's set up correctly + existing_spawner.set_meta("blocking_room", door_data.blocking_room) + spawner_created = true + print("GameWorld: Found existing spawner ", existing_spawner.name, " for door ", door.name) + else: + # Spawn new spawner + var spawner = _spawn_enemy_spawner( + spawner_data.position, + spawner_data.room if spawner_data.has("room") else door_data.blocking_room, + spawner_data # Pass spawner_data to access spawn_once flag + ) + if spawner: + # Store reference to door for spawner (optional - spawner will be found by room trigger) + spawner.set_meta("blocking_room", door_data.blocking_room) + spawner_created = true + print("GameWorld: Spawned enemy spawner ", spawner.name, " for door ", door.name, " at ", spawner_data.position) + if spawner_created: + has_puzzle_element = true + else: + push_warning("GameWorld: WARNING - Failed to spawn enemy spawner for door ", door.name, "!") + if "enemy_spawners" not in door_data: + push_warning("GameWorld: Reason: door_data has no 'enemy_spawners' key!") + elif not door_data.enemy_spawners is Array: + push_warning("GameWorld: Reason: door_data.enemy_spawners is not an Array! Type: ", typeof(door_data.enemy_spawners)) + elif door_data.enemy_spawners.size() == 0: + push_warning("GameWorld: Reason: door_data.enemy_spawners array is empty!") + else: + if "puzzle_type" in door_data: + print("GameWorld: Door ", door.name, " has puzzle_type '", door_data.puzzle_type, "' (not 'enemy')") + + # CRITICAL: If door has no puzzle elements (neither switch nor spawner), this is an error + # This should never happen if dungeon_generator logic is correct, but add safety check + if door_data.type != "KeyDoor" and not has_puzzle_element: + push_error("GameWorld: ERROR - Blocking door ", door.name, " (type: ", door_data.type, ") has no puzzle elements! This door should not exist!") + print("GameWorld: Door data keys: ", door_data.keys()) + print("GameWorld: Door puzzle_type: ", door_data.get("puzzle_type", "MISSING")) + print("GameWorld: Door has requires_switch: ", door_data.get("requires_switch", false)) + print("GameWorld: Door has requires_enemies: ", door_data.get("requires_enemies", false)) + print("GameWorld: Door has floor_switch_position: ", "floor_switch_position" in door_data) + print("GameWorld: Door has enemy_spawners: ", "enemy_spawners" in door_data) + # Remove the door since it's invalid - it was created without puzzle elements + door.queue_free() + print("GameWorld: Removed invalid blocking door ", door.name, " - it had no puzzle elements!") + continue # Skip to next door + + 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 + # 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!") + 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") + + # Set properties + switch.switch_type = switch_type if switch_type == "walk" or switch_type == "pillar" else "walk" + 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) + + # CRITICAL: Store switch_room metadata BEFORE adding to scene + # This ensures switches can be matched to doors in the same room + if switch_room and not switch_room.is_empty(): + switch.set_meta("switch_room", switch_room) + print("GameWorld: Set switch_room metadata for switch - room (", switch_room.x, ", ", switch_room.y, ")") + else: + push_warning("GameWorld: WARNING - Spawning switch without switch_room metadata! This may cause cross-room connections!") + + # Add to scene + var entities_node = get_node_or_null("Entities") + if entities_node: + entities_node.add_child(switch) + switch.global_position = i_position + + # Update tilemap to show switch tile (initial inactive state) + if dungeon_tilemap_layer: + var initial_tile: Vector2i + if switch_type == "pillar": + initial_tile = Vector2i(16, 9) # Pillar switch inactive + else: + initial_tile = Vector2i(11, 9) # Walk-on switch inactive + dungeon_tilemap_layer.set_cell(Vector2i(tile_x, tile_y), 0, initial_tile) + + print("GameWorld: Spawned ", switch_type, " floor switch at ", i_position, " tile (", tile_x, ", ", tile_y, "), room: (", switch_room.get("x", "?") if switch_room and not switch_room.is_empty() else "?", ", ", switch_room.get("y", "?") if switch_room and not switch_room.is_empty() else "?", ")") + return switch + + return null + +func _spawn_enemy_spawner(i_position: Vector2, room: Dictionary, spawner_data: Dictionary = {}) -> Node: + # Spawn an enemy spawner for a blocking room + var spawner_script = load("res://scripts/enemy_spawner.gd") + if not spawner_script: + push_error("ERROR: Could not load enemy_spawner script!") + return null + + var spawner = Node2D.new() + spawner.set_script(spawner_script) + spawner.name = "EnemySpawner_%d_%d" % [room.x, room.y] if room and not room.is_empty() else "EnemySpawner_%d_%d" % [int(i_position.x), int(i_position.y)] + spawner.add_to_group("enemy_spawner") + + # Set spawner properties - IMPORTANT: spawn_on_ready = false so enemies only spawn when player enters room + spawner.spawn_on_ready = false # Don't spawn on ready - wait for room trigger + spawner.respawn_time = 0.0 # Don't respawn - enemies spawn once when entering room + spawner.max_enemies = 1 # One enemy per spawner + + # Check if this spawner should be destroyed after spawning once + if spawner_data.has("spawn_once") and spawner_data.spawn_once: + spawner.set_meta("spawn_once", true) # Mark spawner for destruction after spawning + + # Set enemy scenes (use default enemy types) + # enemy_scenes is Array[PackedScene], so we need to properly type it + var enemy_scenes: Array[PackedScene] = [] + var scene_paths = [ + "res://scenes/enemy_rat.tscn", + "res://scenes/enemy_humanoid.tscn", + "res://scenes/enemy_slime.tscn", + "res://scenes/enemy_bat.tscn" + ] + + # Load scenes and add to typed array + for path in scene_paths: + var scene = load(path) as PackedScene + if scene: + enemy_scenes.append(scene) + + spawner.enemy_scenes = enemy_scenes + + # Set multiplayer authority + if multiplayer.has_multiplayer_peer(): + spawner.set_multiplayer_authority(1) + + # Store room reference + if room and not room.is_empty(): + spawner.set_meta("room", room) + + # Add to scene + var entities_node = get_node_or_null("Entities") + if entities_node: + entities_node.add_child(spawner) + spawner.global_position = i_position + print("GameWorld: ✓✓✓ Successfully spawned enemy spawner '", spawner.name, "' at ", i_position, " for room at (", room.x if room and not room.is_empty() else "unknown", ", ", room.y if room and not room.is_empty() else "unknown", ")") + print("GameWorld: Spawner has room metadata: ", spawner.has_meta("room")) + if spawner.has_meta("room"): + var spawner_room = spawner.get_meta("room") + print("GameWorld: Spawner room metadata: (", spawner_room.x if spawner_room and not spawner_room.is_empty() else "none", ", ", spawner_room.y if spawner_room and not spawner_room.is_empty() else "none", ", ", spawner_room.w if spawner_room and not spawner_room.is_empty() else "none", "x", spawner_room.h if spawner_room and not spawner_room.is_empty() else "none", ")") + print("GameWorld: Spawner in group 'enemy_spawner': ", spawner.is_in_group("enemy_spawner")) + print("GameWorld: Spawner enemy_scenes.size(): ", spawner.enemy_scenes.size() if "enemy_scenes" in spawner else "N/A") + return spawner + + return null + +func _spawn_room_triggers(): + # Spawn room trigger areas for all rooms + if dungeon_data.is_empty() or not dungeon_data.has("rooms"): + return + + var rooms = dungeon_data.rooms + if rooms == null or not rooms is Array: + return + + var trigger_script = load("res://scripts/room_trigger.gd") + if not trigger_script: + push_error("ERROR: Could not load room_trigger script!") + return + + var entities_node = get_node_or_null("Entities") + if not entities_node: + push_error("ERROR: Could not find Entities node!") + return + + print("GameWorld: Spawning ", rooms.size(), " room triggers") + + for i in range(rooms.size()): + var room = rooms[i] + if not room is Dictionary: + continue + + var trigger = Area2D.new() + trigger.set_script(trigger_script) + trigger.name = "RoomTrigger_%d" % i + trigger.add_to_group("room_trigger") + + # Set room data + trigger.room = room + + # Create collision shape covering ONLY the room interior (no overlap with adjacent rooms) + var collision_shape = CollisionShape2D.new() + var rect_shape = RectangleShape2D.new() + var tile_size = 16 + # Room interior is from room.x + 2 to room.x + room.w - 2 (excluding 2-tile walls) + # This ensures the trigger only covers THIS room, not adjacent rooms or doorways + var room_world_x = (room.x + 2) * tile_size + var room_world_y = (room.y + 2) * tile_size + var room_world_w = (room.w - 4) * tile_size # Width excluding 2-tile walls on each side + var room_world_h = (room.h - 4) * tile_size # Height excluding 2-tile walls on each side + rect_shape.size = Vector2(room_world_w, room_world_h) + collision_shape.shape = rect_shape + # Position collision shape at center of room (relative to Area2D) + collision_shape.position = Vector2(room_world_w / 2.0, room_world_h / 2.0) + trigger.add_child(collision_shape) + + # Set Area2D global position to the top-left corner of the room interior + # This ensures the trigger ONLY covers this specific room + trigger.global_position = Vector2(room_world_x, room_world_y) + + # Set multiplayer authority + if multiplayer.has_multiplayer_peer(): + trigger.set_multiplayer_authority(1) + + # Add to scene + entities_node.add_child(trigger) + + print("GameWorld: Spawned ", rooms.size(), " room triggers") + +func _place_key_in_room(room: Dictionary): + # Place a key in the specified room (as loot) + if room.is_empty(): + return + + var loot_scene = preload("res://scenes/loot.tscn") + if not loot_scene: + return + + var entities_node = get_node_or_null("Entities") + if not entities_node: + return + + # Find a valid floor position in the room + var tile_size = 16 + var valid_positions = [] + + # Room interior is from room.x + 2 to room.x + room.w - 2 + for x in range(room.x + 2, room.x + room.w - 2): + for y in range(room.y + 2, room.y + room.h - 2): + if x >= 0 and x < dungeon_data.map_size.x and y >= 0 and y < dungeon_data.map_size.y: + if dungeon_data.grid[x][y] == 1: # Floor + var world_x = x * tile_size + tile_size / 2.0 + var world_y = y * tile_size + tile_size / 2.0 + valid_positions.append(Vector2(world_x, world_y)) + + if valid_positions.size() > 0: + # Pick a random position + var rng = RandomNumberGenerator.new() + rng.randomize() + var key_pos = valid_positions[rng.randi() % valid_positions.size()] + + # Spawn key loot + var key_loot = loot_scene.instantiate() + key_loot.name = "KeyLoot_%d_%d" % [int(key_pos.x), int(key_pos.y)] + key_loot.loot_type = key_loot.LootType.KEY + + # Set multiplayer authority + if multiplayer.has_multiplayer_peer(): + key_loot.set_multiplayer_authority(1) + + entities_node.add_child(key_loot) + key_loot.global_position = key_pos + + print("GameWorld: Placed key in room at ", key_pos) + +func _place_pillar_in_room(room: Dictionary, switch_position: Vector2): + # Place a pillar in the specified room (needed for pillar switches) + if room.is_empty(): + return + + var interactable_object_scene = preload("res://scenes/interactable_object.tscn") + if not interactable_object_scene: + push_error("ERROR: Could not load interactable_object scene for pillar!") + return + + var entities_node = get_node_or_null("Entities") + if not entities_node: + push_error("ERROR: Could not find Entities node for pillar placement!") + return + + # Find a valid floor position in the room (away from the switch) + var tile_size = 16 + var valid_positions = [] + + # Room interior is from room.x + 2 to room.x + room.w - 2 (excluding 2-tile walls) + # CRITICAL: Also exclude last row/column of floor tiles to prevent objects from spawning in walls + # Objects are 16x16, so we need at least 1 tile buffer from walls + # Floor tiles are at: room.x+2, room.x+3, ..., room.x+room.w-3 (last floor tile before wall) + # To exclude last tile, we want: room.x+2, room.x+3, ..., room.x+room.w-4 + var min_x = room.x + 2 + var max_x = room.x + room.w - 4 # Exclude last 2 floor tiles (room.w-3 is last floor, room.w-4 is second-to-last) + var 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) + + 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: + if dungeon_data.grid[x][y] == 1: # Floor + # CRITICAL: Interactable objects are 16x16 pixels with origin at (0,0) (top-left) + # To center a 16x16 sprite in a 16x16 tile, we need to offset by 8 pixels into the tile + # Tile (x,y) spans from (x*16, y*16) to ((x+1)*16, (y+1)*16) + # Position sprite 8 pixels into the tile from top-left: (x*16 + 8, y*16 + 8) + var world_x = x * tile_size + 8 + 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 + var distance_to_switch = world_pos.distance_to(switch_position) + if distance_to_switch >= tile_size * 2: # At least 2 tiles away + valid_positions.append(world_pos) + + if valid_positions.size() > 0: + # Pick a random position + var rng = RandomNumberGenerator.new() + rng.randomize() + var pillar_pos = valid_positions[rng.randi() % valid_positions.size()] + + # Spawn pillar interactable object + var pillar = interactable_object_scene.instantiate() + pillar.name = "Pillar_%d_%d" % [int(pillar_pos.x), int(pillar_pos.y)] + pillar.set_meta("dungeon_spawned", true) + pillar.set_meta("room", room) + + # Set multiplayer authority + if multiplayer.has_multiplayer_peer(): + pillar.set_multiplayer_authority(1) + + # Add to scene tree + entities_node.add_child(pillar) + pillar.global_position = pillar_pos + + # Call setup function to configure as pillar + if pillar.has_method("setup_pillar"): + pillar.call("setup_pillar") + else: + push_error("ERROR: Pillar does not have setup_pillar method!") + + # Add to group for easy access + pillar.add_to_group("interactable_object") + + print("GameWorld: Placed pillar in room at ", pillar_pos, " (switch at ", switch_position, ")") + else: + push_warning("GameWorld: Could not find valid position for pillar in room! Room might be too small.") + +func _connect_door_to_room_trigger(door: Node): + # Connect a door to its room trigger area + # blocking_room is a variable in door.gd, so it should exist + var blocking_room = door.blocking_room + if not blocking_room or blocking_room.is_empty(): + return + + # Find the room trigger for this room + for trigger in get_tree().get_nodes_in_group("room_trigger"): + if is_instance_valid(trigger): + # room is a variable in room_trigger.gd, compare by values + var trigger_room = trigger.room + if trigger_room and not trigger_room.is_empty() and \ + trigger_room.x == blocking_room.x and trigger_room.y == blocking_room.y and \ + trigger_room.w == blocking_room.w and trigger_room.h == blocking_room.h: + # Connect door to trigger + door.room_trigger_area = trigger + # Add door to trigger's doors list (doors_in_room is a variable in room_trigger.gd) + trigger.doors_in_room.append(door) + break diff --git a/src/scripts/interactable_object.gd b/src/scripts/interactable_object.gd index 5aa7f3a..3f9387f 100644 --- a/src/scripts/interactable_object.gd +++ b/src/scripts/interactable_object.gd @@ -291,7 +291,15 @@ func can_be_destroyed() -> bool: func on_grabbed(by_player): # Special handling for chests - open instead of grab if object_type == "Chest" and not is_chest_opened: - _open_chest() + # 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(): + var player_peer_id = by_player.get_multiplayer_authority() + _request_chest_open.rpc_id(1, player_peer_id) + else: + # Server or single player - open directly + _open_chest(by_player) return is_being_held = true @@ -463,46 +471,120 @@ func setup_pushable_high_box(): if sprite_above: sprite_above.frame = top_frames[index] -func _open_chest(): +func _open_chest(by_player: Node = null): if is_chest_opened: return + # Only process on server (authority) + if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority(): + return + is_chest_opened = true if sprite and chest_opened_frame >= 0: sprite.frame = chest_opened_frame - # Spawn loot item - var loot_scene = preload("res://scenes/loot.tscn") - if loot_scene: - var loot = loot_scene.instantiate() - if loot: - # Random loot type - var loot_types = loot.LootType.values() - loot.loot_type = loot_types[randi() % loot_types.size()] - - # Position above chest with some randomness - var spawn_pos = global_position + Vector2(randf_range(-10, 10), randf_range(-20, -10)) - loot.global_position = spawn_pos - - # Set initial velocity to fly out - var random_angle = randf() * PI * 2 - var random_force = randf_range(80.0, 120.0) - loot.velocity = Vector2(cos(random_angle), sin(random_angle)) * random_force - loot.velocity_z = randf_range(100.0, 150.0) - loot.is_airborne = true - loot.velocity_set_by_spawner = true - - get_parent().call_deferred("add_child", loot) - - # Sync to network if multiplayer - if multiplayer.has_multiplayer_peer(): - _sync_chest_open.rpc() - - print(name, " opened! Loot spawned: ", loot_types[loot.loot_type]) + # Random loot type + var loot_types = [ + {"type": "coin", "name": "Coin", "color": Color(1.0, 0.84, 0.0)}, + {"type": "apple", "name": "Apple", "color": Color.GREEN}, + {"type": "banana", "name": "Banana", "color": Color.YELLOW}, + {"type": "cherry", "name": "Cherry", "color": Color.RED}, + {"type": "key", "name": "Key", "color": Color.YELLOW} + ] + var selected_loot = loot_types[randi() % loot_types.size()] + + # CRITICAL: Instantly give item to player instead of spawning loot object + if by_player and is_instance_valid(by_player) and by_player.is_in_group("player"): + # Give item directly to player based on type + match selected_loot.type: + "coin": + if by_player.has_method("add_coins"): + 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) + "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) + "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) + "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) + "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) + + # Play chest open sound + if has_node("SfxChestOpen"): + $SfxChestOpen.play() + + print(name, " opened by ", by_player.name, "! Item given: ", selected_loot.name) + else: + push_error("Chest: ERROR - No valid player to give item to!") + + # Sync chest opening visual to all clients (item already given on server) + if multiplayer.has_multiplayer_peer(): + _sync_chest_open.rpc(selected_loot.type if by_player else "coin") @rpc("any_peer", "reliable") -func _sync_chest_open(): - # Sync chest opening to all clients +func _request_chest_open(player_peer_id: int): + # Server receives chest open request from client + if not multiplayer.is_server(): + return + + if is_chest_opened: + return + + # Find the player by peer ID + var player = null + var players = get_tree().get_nodes_in_group("player") + for p in players: + if p.get_multiplayer_authority() == player_peer_id: + player = p + break + + if not player: + push_error("Chest: ERROR - Could not find player with peer_id ", player_peer_id, " for chest opening!") + return + + # Open chest on server (this will give item to player) + _open_chest(player) + +@rpc("any_peer", "reliable") +func _sync_chest_open(_loot_type_str: String = "coin"): + # Sync chest opening to all clients (only visual - item already given on server) if not is_chest_opened and sprite and chest_opened_frame >= 0: is_chest_opened = true sprite.frame = chest_opened_frame + + # Play chest open sound on clients + if has_node("SfxChestOpen"): + $SfxChestOpen.play() + +func _show_item_pickup_notification(player: Node, text: String, text_color: Color, item_texture: Texture2D = null, sprite_hframes: int = 1, sprite_vframes: int = 1, sprite_frame: int = 0): + # Show item graphic and text above player's head for 0.5s, then fade out over 0.5s + var floating_text_scene = preload("res://scenes/floating_text.tscn") + if floating_text_scene and player and is_instance_valid(player): + var floating_text = floating_text_scene.instantiate() + var parent = player.get_parent() + if parent: + 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 diff --git a/src/scripts/level_text_ui.gd b/src/scripts/level_text_ui.gd index a583dea..9999829 100644 --- a/src/scripts/level_text_ui.gd +++ b/src/scripts/level_text_ui.gd @@ -23,6 +23,9 @@ func show_level(level_number: int): if tween.is_valid(): tween.kill() + # Get vbox once at the start (reuse throughout function) + var vbox = get_child(0) if get_child_count() > 0 else null + # Update label text FIRST before showing if level_label: level_label.text = "LEVEL " + str(level_number) @@ -30,7 +33,6 @@ func show_level(level_number: int): else: print("LevelTextUI: ERROR - level_label is null!") # Try to find it again - var vbox = get_child(0) if get_child_count() > 0 else null if vbox: for child in vbox.get_children(): if child.name == "LevelLabel" or child is Label: @@ -43,7 +45,6 @@ func show_level(level_number: int): visible = true # Fade in - fade the VBoxContainer and level label - var vbox = get_child(0) if get_child_count() > 0 else null if vbox: vbox.modulate.a = 0.0 var fade_in = create_tween() @@ -68,4 +69,3 @@ func show_level(level_number: int): await fade_out.finished visible = false - diff --git a/src/scripts/loot.gd b/src/scripts/loot.gd index 82bb781..7aeeeeb 100644 --- a/src/scripts/loot.gd +++ b/src/scripts/loot.gd @@ -102,28 +102,28 @@ func _setup_sprite(): sprite.texture = items_texture sprite.hframes = 20 sprite.vframes = 14 - sprite.frame = (8 * 20) + 11 # vframe 9, hframe 11 + sprite.frame = (8 * 20) + 11 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 # vframe 9, hframe 12 + sprite.frame = (8 * 20) + 12 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 # vframe 9, hframe 13 + sprite.frame = (8 * 20) + 13 LootType.KEY: 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 = (13 * 20) + 11 # vframe 9, hframe 13 + sprite.frame = (13 * 20) + 10 func _setup_collision_shape(): if not collision_shape: @@ -338,8 +338,9 @@ func _process_pickup_on_server(player: Node): # Give player coin if player.has_method("add_coins"): player.add_coins(coin_value) - # Show floating text (gold color) - _show_floating_text(player, "+1 coin", Color(1.0, 0.84, 0.0)) # Gold color + # 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) self.visible = false @@ -347,14 +348,53 @@ func _process_pickup_on_server(player: Node): if sfx_coin_collect and sfx_coin_collect.playing: await sfx_coin_collect.finished queue_free() - LootType.APPLE, LootType.BANANA, LootType.CHERRY: + LootType.APPLE: if sfx_potion_collect: sfx_potion_collect.play() # Heal player + var actual_heal = 0.0 if player.has_method("heal"): + actual_heal = heal_amount player.heal(heal_amount) - # Show floating text - _show_floating_text(player, "+" + str(int(heal_amount)), Color.GREEN) + # 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) + + self.visible = false + + # Wait for sound to finish before removing + if sfx_potion_collect and sfx_potion_collect.playing: + await sfx_potion_collect.finished + queue_free() + LootType.BANANA: + if sfx_potion_collect: + sfx_potion_collect.play() + # Heal player + var actual_heal = 0.0 + if player.has_method("heal"): + actual_heal = heal_amount + 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) + + self.visible = false + + # Wait for sound to finish before removing + if sfx_potion_collect and sfx_potion_collect.playing: + await sfx_potion_collect.finished + queue_free() + LootType.CHERRY: + if sfx_potion_collect: + sfx_potion_collect.play() + # Heal player + var actual_heal = 0.0 + if player.has_method("heal"): + actual_heal = heal_amount + 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) self.visible = false @@ -365,9 +405,12 @@ func _process_pickup_on_server(player: Node): LootType.KEY: if sfx_key_collect: sfx_key_collect.play() - # TODO: GIVE PLAYER KEY IN INVENTORY! - # Show floating text - _show_floating_text(player, "+1 key", Color.YELLOW) + # Give player key in inventory + if player.has_method("add_key"): + 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) self.visible = false @@ -458,23 +501,26 @@ func _sync_remove(): visible = false # Wait for sound to finish before removing (if sound is playing) - var sound_playing = false + var _sound_playing = false if loot_type == LootType.COIN and sfx_coin_collect and sfx_coin_collect.playing: - sound_playing = true + _sound_playing = true await sfx_coin_collect.finished elif loot_type in [LootType.APPLE, LootType.BANANA, LootType.CHERRY] and sfx_loot_collect and sfx_loot_collect.playing: - sound_playing = true + _sound_playing = true await sfx_loot_collect.finished # Remove from scene if not is_queued_for_deletion(): queue_free() -func _show_floating_text(player: Node, text: String, color: Color): - # Create floating text above player +func _show_floating_text(player: Node, text: String, color: Color, show_time: float = 0.5, fade_time: float = 0.5, item_texture: Texture2D = null, sprite_hframes: int = 1, sprite_vframes: int = 1, sprite_frame: int = 0): + # Create floating text and item graphic above player's head + # Shows for show_time seconds, then fades out over fade_time seconds var floating_text_scene = preload("res://scenes/floating_text.tscn") - if floating_text_scene: + if floating_text_scene and player and is_instance_valid(player): var floating_text = floating_text_scene.instantiate() - player.get_parent().add_child(floating_text) - floating_text.global_position = player.global_position + Vector2(0, -20) - floating_text.setup(text, color) + var parent = player.get_parent() + if parent: + parent.add_child(floating_text) + floating_text.global_position = player.global_position + Vector2(0, -20) + floating_text.setup(text, color, show_time, fade_time, item_texture, sprite_hframes, sprite_vframes, sprite_frame) diff --git a/src/scripts/player.gd b/src/scripts/player.gd index c9a1651..ae6d173 100644 --- a/src/scripts/player.gd +++ b/src/scripts/player.gd @@ -107,6 +107,9 @@ var coins: int: if character_stats: character_stats.coin = value +# Key inventory +var keys: int = 0 # Number of keys the player has + # Animation system enum Direction { DOWN = 0, @@ -2065,7 +2068,10 @@ func take_damage(amount: float, attacker_position: Vector2): tween.tween_property(sprite_body, "modulate", Color.RED, 0.1) tween.tween_property(sprite_body, "modulate", Color.WHITE, 0.1) - # Sync damage visual effects to other clients + # Show damage number (red, using dmg_numbers.png font) + _show_damage_number(amount, attacker_position) + + # Sync damage visual effects to other clients (including damage numbers) if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree(): _sync_damage.rpc(amount, attacker_position) @@ -2351,7 +2357,59 @@ func heal(amount: float): current_health = min(current_health + amount, max_health) print(name, " healed for ", amount, " HP! Health: ", current_health, "/", max_health) +func add_key(amount: int = 1): + keys += amount + print(name, " picked up ", amount, " key(s)! Total keys: ", keys) + +func has_key() -> bool: + return keys > 0 + +func use_key(): + if keys > 0: + keys -= 1 + print(name, " used a key! Remaining keys: ", keys) + return true + return false + @rpc("authority", "reliable") +func _show_damage_number(amount: float, from_position: Vector2): + # Show damage number (red, using dmg_numbers.png font) above player + # Only show if damage > 0 + if amount <= 0: + return + + var damage_number_scene = preload("res://scenes/damage_number.tscn") + if not damage_number_scene: + return + + var damage_label = damage_number_scene.instantiate() + if not damage_label: + return + + # Set damage text and red color + damage_label.label = str(int(amount)) + damage_label.color = Color.RED + + # Calculate direction from attacker (slight upward variation) + var direction_from_attacker = (global_position - from_position).normalized() + # Add slight upward bias + direction_from_attacker = direction_from_attacker.lerp(Vector2(0, -1), 0.5).normalized() + damage_label.direction = direction_from_attacker + + # Position above player's head + var game_world = get_tree().get_first_node_in_group("game_world") + if game_world: + var entities_node = game_world.get_node_or_null("Entities") + if entities_node: + entities_node.add_child(damage_label) + damage_label.global_position = global_position + Vector2(0, -16) # Above player head + else: + get_tree().current_scene.add_child(damage_label) + damage_label.global_position = global_position + Vector2(0, -16) + else: + get_tree().current_scene.add_child(damage_label) + damage_label.global_position = global_position + Vector2(0, -16) + func _sync_damage(_amount: float, attacker_position: Vector2): # This RPC only syncs visual effects, not damage application # (damage is already applied via rpc_take_damage) diff --git a/src/scripts/room_trigger.gd b/src/scripts/room_trigger.gd new file mode 100644 index 0000000..e966377 --- /dev/null +++ b/src/scripts/room_trigger.gd @@ -0,0 +1,605 @@ +extends Area2D + +# Room Trigger - Detects when players enter/exit a room + +var room: Dictionary = {} # Room data this trigger covers +var players_in_room: Array = [] # Players currently in the room +var doors_in_room: Array = [] # Doors that belong to this room +var enemies_in_room: Array = [] # Enemies in this room +var floor_switches_in_room: Array = [] # Floor switches in this room +var enemy_spawners: Array = [] # Enemy spawners for this room +var enemies_spawned: bool = false # True if enemies have been spawned for this room +var debug_label: Label = null # Debug label showing puzzle type and trigger status +var room_entered: bool = false # True if player has entered this room + +func _ready(): + # Set collision mask to detect players + collision_layer = 0 + collision_mask = 1 # Detect players (layer 1) + + # Connect signals + 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") + +func _on_body_entered(body): + # Player entered the room + if body.is_in_group("player"): + if not body in players_in_room: + players_in_room.append(body) + _on_player_entered_room(body) + +func _on_body_exited(body): + # Player left the room + if body.is_in_group("player"): + if body in players_in_room: + players_in_room.erase(body) + _on_player_exited_room(body) + +func _on_player_entered_room(player: Node): + # Handle player entering room + print("Player ", player.name, " entered room at ", room.x, ", ", room.y) + print("RoomTrigger: This trigger is for room (", room.x, ", ", room.y, ", ", room.w, "x", room.h, ")") + print("RoomTrigger: Found ", doors_in_room.size(), " doors in this room") + + # Mark room as entered and update debug label + room_entered = true + _update_debug_label() + + # Check if this room has a puzzle (has blocking doors IN this room) + # If this room has a puzzle, close the doors IN this room (blocking exits) + # doors_in_room contains doors where room1 == this room OR blocking_room == this room (doors IN this room) + for door in doors_in_room: + if not is_instance_valid(door): + continue + + # CRITICAL: Verify this door is actually IN this room + # A door is IN a room if door.room1 == this room OR door.blocking_room == this room + var door_room1 = door.room1 if door.room1 else {} + var door_blocking_room = door.blocking_room if door.blocking_room else {} + var door_in_this_room = false + + # Check if door.room1 matches this room (door starts FROM this room) + if door_room1 and not door_room1.is_empty(): + door_in_this_room = (door_room1.x == room.x and door_room1.y == room.y and \ + door_room1.w == room.w and door_room1.h == room.h) + + # Also check blocking_room + if not door_in_this_room and door_blocking_room and not door_blocking_room.is_empty(): + door_in_this_room = (door_blocking_room.x == room.x and door_blocking_room.y == room.y and \ + door_blocking_room.w == room.w and door_blocking_room.h == room.h) + + if not door_in_this_room: + # Door is NOT in this room - DO NOT call it! + print("RoomTrigger: ERROR - Door ", door.name, " is NOT in room (", room.x, ", ", room.y, ")!") + print("RoomTrigger: Door room1: (", door_room1.x if door_room1 else "none", ", ", door_room1.y if door_room1 else "none", ")") + print("RoomTrigger: Door blocking_room: (", door_blocking_room.x if door_blocking_room else "none", ", ", door_blocking_room.y if door_blocking_room else "none", ")") + print("RoomTrigger: Removing from this trigger's doors list!") + doors_in_room.erase(door) + if door.room_trigger_area == self: + door.room_trigger_area = null + continue + + # Door is in this room - verify it's connected to this trigger + if door.room_trigger_area != self: + door.room_trigger_area = self + + # Call the door's _on_room_entered to close it (if puzzle not solved) + if door.has_method("_on_room_entered"): + door._on_room_entered(player) + + # Spawn enemies if this room has a spawner + print("RoomTrigger: About to call _spawn_room_enemies()...") + _spawn_room_enemies() + print("RoomTrigger: Finished _spawn_room_enemies()") + +func _on_player_exited_room(player: Node): + # Handle player leaving room + print("Player ", player.name, " exited room at ", room.x, ", ", room.y) + +func _find_room_entities(): + # Find all doors, enemies, and switches that belong to this room + var game_world = get_tree().get_first_node_in_group("game_world") + if not game_world: + return + + var entities_node = game_world.get_node_or_null("Entities") + if not entities_node: + return + + # Find doors - search in Entities node + # CRITICAL: Find doors where room1 == THIS room OR blocking_room == THIS room + # Blocking doors are IN the puzzle room (they lead OUT OF this room) + # When you enter this room, doors IN this room should close (blocking exits) + print("RoomTrigger: Finding doors for room (", room.x, ", ", room.y, ", ", room.w, "x", room.h, ")") + var _total_blocking_doors = 0 + for child in entities_node.get_children(): + if child.is_in_group("blocking_door") or child.name.begins_with("BlockingDoor_"): + _total_blocking_doors += 1 + if not is_instance_valid(child): + print("RoomTrigger: Door ", child.name, " is invalid, skipping") + continue + + # Check if door is IN this room (room1 == this room OR blocking_room == this room) + var door_in_this_room = false + var door_room1 = child.room1 if child.room1 else {} + var door_blocking_room = child.blocking_room if child.blocking_room else {} + + # CRITICAL: For blocking doors (StoneDoor/GateDoor), BOTH room1 AND blocking_room should match this room + # For KeyDoors, only room1 needs to match + var is_blocking_door = (child.type == "StoneDoor" or child.type == "GateDoor") + + # Check room1 first (door starts FROM this room) + if door_room1 and not door_room1.is_empty(): + var room1_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 room1_matches: + door_in_this_room = true + print("RoomTrigger: Door ", child.name, " room1 matches this room (door IN this room)") + + # For blocking doors, also verify blocking_room matches (it should match room1) + if door_in_this_room and is_blocking_door and door_blocking_room and not door_blocking_room.is_empty(): + var blocking_matches = (door_blocking_room.x == room.x and door_blocking_room.y == room.y and \ + door_blocking_room.w == room.w and door_blocking_room.h == room.h) + if not blocking_matches: + # Blocking door's blocking_room doesn't match - this is an error! + print("RoomTrigger: ERROR - Blocking door ", child.name, " room1 matches but blocking_room (", door_blocking_room.x, ", ", door_blocking_room.y, ") doesn't match this room (", room.x, ", ", room.y, ")!") + door_in_this_room = false # Reject this door + else: + print("RoomTrigger: Blocking door ", child.name, " blocking_room also matches this room (verified)") + + # For non-blocking doors or if room1 didn't match, check blocking_room as fallback + if not door_in_this_room and door_blocking_room and not door_blocking_room.is_empty(): + door_in_this_room = (door_blocking_room.x == room.x and door_blocking_room.y == room.y and \ + door_blocking_room.w == room.w and door_blocking_room.h == room.h) + if door_in_this_room: + print("RoomTrigger: Door ", child.name, " blocking_room matches this room (fallback check)") + else: + print("RoomTrigger: Door ", child.name, " blocking_room (", door_blocking_room.x if door_blocking_room else "none", ", ", door_blocking_room.y if door_blocking_room else "none", ") doesn't match this room (", room.x, ", ", room.y, ")") + + if not door_room1 and not door_blocking_room: + print("RoomTrigger: Door ", child.name, " has no room1 or blocking_room set!") + + if door_in_this_room: + # This door is IN THIS room (blocks exits from this room) - add it to this trigger + # CRITICAL: Check if door is already connected to a different trigger FIRST + # This prevents doors from being connected to multiple triggers + if child.room_trigger_area and child.room_trigger_area != self: + # Door already connected to a different trigger - this is an ERROR! + var other_trigger_room = child.room_trigger_area.room if child.room_trigger_area.room else {} + var other_room_str = "(" + str(other_trigger_room.x if other_trigger_room else "none") + ", " + str(other_trigger_room.y if other_trigger_room else "none") + ")" + var this_room_str = "(" + str(room.x) + ", " + str(room.y) + ")" + + # Debug: Print door's room info to understand why it's matching multiple rooms + print("RoomTrigger: WARNING - Door ", child.name, " already connected to trigger for room ", other_room_str, "!") + print("RoomTrigger: Door room1: (", door_room1.x if door_room1 else "none", ", ", door_room1.y if door_room1 else "none", ")") + print("RoomTrigger: Door blocking_room: (", door_blocking_room.x if door_blocking_room else "none", ", ", door_blocking_room.y if door_blocking_room else "none", ")") + print("RoomTrigger: Current trigger room: ", this_room_str) + print("RoomTrigger: Skipping this door - it belongs to the other trigger!") + + # Don't add to this trigger if already connected to another trigger + if child in doors_in_room: + doors_in_room.erase(child) + continue # Skip this door entirely + + # Door is not connected to another trigger - safe to add + # CRITICAL: Only add if not already in the list (avoid duplicates) + if not child in doors_in_room: + doors_in_room.append(child) + print("RoomTrigger: Added door ", child.name, " that is IN room (", room.x, ", ", room.y, ") to this trigger") + + # Set door's room trigger reference (should be null at this point, but set it anyway) + if not child.room_trigger_area: + child.room_trigger_area = self + else: + # This door is NOT in this room - ensure it's not in this trigger's list + # This prevents doors from being accidentally connected to wrong triggers + if child in doors_in_room: + print("RoomTrigger: Removing door ", child.name, " from this trigger - it's not in this room (", room.x, ", ", room.y, ")") + doors_in_room.erase(child) + if child.room_trigger_area == self: + print("RoomTrigger: Disconnecting door ", child.name, " from this trigger - wrong room!") + child.room_trigger_area = null + + print("RoomTrigger: Found ", doors_in_room.size(), " doors for room (", room.x, ", ", room.y, ")") + + # Find enemies + for child in entities_node.get_children(): + if child.is_in_group("enemy"): + # Check if enemy is in this room + if child.has_meta("room"): + var enemy_room = child.get_meta("room") + if enemy_room == room: + enemies_in_room.append(child) + else: + # Check by position + 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 = room.x + 2 + var room_max_x = room.x + room.w - 2 + var room_min_y = room.y + 2 + var room_max_y = room.y + 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: + enemies_in_room.append(child) + + # Find floor switches + for child in get_tree().get_nodes_in_group("floor_switch"): + if 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) + var switch_tile_y = int(child.global_position.y / tile_size) + var room_min_x = room.x + 2 + var room_max_x = room.x + room.w - 2 + var room_min_y = room.y + 2 + var room_max_y = room.y + room.h - 2 + + if switch_tile_x >= room_min_x and switch_tile_x < room_max_x and \ + switch_tile_y >= room_min_y and switch_tile_y < room_max_y: + floor_switches_in_room.append(child) + + # Update debug label after finding all entities + call_deferred("_update_debug_label") + +func _spawn_room_enemies(): + print("RoomTrigger: ===== _spawn_room_enemies() CALLED for room (", room.x, ", ", room.y, ") =====") + + # Spawn enemies when player enters room (if room has spawners and not already spawned) + if enemies_spawned: + print("RoomTrigger: Already spawned enemies, skipping...") + return # Already spawned enemies + + # CRITICAL: Remove any existing smoke puffs before spawning new ones + _cleanup_smoke_puffs() + + # Find enemy spawners for this room + _find_room_spawners() + + print("RoomTrigger: ===== Found ", enemy_spawners.size(), " spawners for room (", room.x, ", ", room.y, ") =====") + + # Spawn enemies from all spawners in this room (only once) + if enemy_spawners.size() > 0: + for spawner in enemy_spawners: + if not is_instance_valid(spawner): + print("RoomTrigger: WARNING - Invalid spawner found, skipping") + continue + + if not spawner.has_method("spawn_enemy"): + print("RoomTrigger: WARNING - Spawner ", spawner.name, " doesn't have spawn_enemy method!") + continue + + # CRITICAL: Verify spawner has enemy scenes set + if "enemy_scenes" in spawner: + if spawner.enemy_scenes.size() == 0: + print("RoomTrigger: ERROR - Spawner ", spawner.name, " has empty enemy_scenes array! Cannot spawn!") + continue + else: + print("RoomTrigger: Spawner ", spawner.name, " has ", spawner.enemy_scenes.size(), " enemy scenes available") + else: + print("RoomTrigger: ERROR - Spawner ", spawner.name, " has no enemy_scenes property!") + continue + + # CRITICAL: Verify spawner is on server (authority) - only server can spawn + if multiplayer.has_multiplayer_peer() and not spawner.is_multiplayer_authority(): + print("RoomTrigger: WARNING - Spawner ", spawner.name, " is not multiplayer authority! Cannot spawn on client!") + continue + + print("RoomTrigger: Calling spawn_enemy() on spawner ", spawner.name, " at ", spawner.global_position) + # Spawn enemies from this spawner (spawner will handle max_enemies check) + # NOTE: spawn_enemy() is async (uses await), so we don't await it here - it will execute asynchronously + spawner.spawn_enemy() + + enemies_spawned = true + print("RoomTrigger: Called spawn_enemy() on ", enemy_spawners.size(), " spawners in room at ", room.x, ", ", room.y) + + # Update debug label + _update_debug_label() + + # Wait a bit for enemies to actually spawn (since spawn_enemy() waits for smoke puffs first) + # Give it enough time for smoke puffs to finish (2.4s) plus a small buffer + await get_tree().create_timer(3.0).timeout + _find_room_entities() # Refresh enemy list after spawning completes + _update_debug_label() # Update again after enemies spawn + else: + print("RoomTrigger: No spawners found for room (", room.x, ", ", room.y, ")") + _update_debug_label() + +func _cleanup_smoke_puffs(): + # Remove all existing smoke puffs in the scene before spawning new ones + var entities_node = get_tree().get_first_node_in_group("game_world") + if not entities_node: + entities_node = get_node_or_null("/root/GameWorld/Entities") + + if not entities_node: + return + + # Find and remove all smoke puffs + var smoke_puffs_removed = 0 + for child in entities_node.get_children(): + if child.is_in_group("smoke_puff") or child.name.begins_with("SmokePuff"): + print("RoomTrigger: Removing existing smoke puff: ", child.name) + child.queue_free() + smoke_puffs_removed += 1 + + if smoke_puffs_removed > 0: + print("RoomTrigger: Cleaned up ", smoke_puffs_removed, " existing smoke puffs before spawning") + +func _find_room_spawners(): + # CRITICAL: Clear the list first to avoid accumulating old spawners + enemy_spawners.clear() + + # Find enemy spawners in this room + var game_world = get_tree().get_first_node_in_group("game_world") + if not game_world: + print("RoomTrigger: ERROR - No game_world found when searching for spawners!") + return + + var entities_node = game_world.get_node_or_null("Entities") + if not entities_node: + print("RoomTrigger: ERROR - No Entities node found when searching for spawners!") + return + + print("RoomTrigger: ===== Searching for spawners in room (", room.x, ", ", room.y, ", ", room.w, "x", room.h, ") =====") + print("RoomTrigger: Entities node has ", entities_node.get_child_count(), " children") + + # Search for spawners (they might be direct children of Entities or in a Spawners node) + var found_spawners_count = 0 + for child in entities_node.get_children(): + if child.name.begins_with("EnemySpawner_") or child.is_in_group("enemy_spawner"): + found_spawners_count += 1 + print("RoomTrigger: Checking spawner: ", child.name, " at ", child.global_position) + + if not is_instance_valid(child): + print("RoomTrigger: Spawner is invalid, skipping") + continue + + var spawner_in_room = false + + # First check if spawner has room metadata matching this room + if child.has_meta("room"): + var spawner_room = child.get_meta("room") + print("RoomTrigger: Spawner has room metadata: (", spawner_room.x if spawner_room and not spawner_room.is_empty() else "none", ", ", spawner_room.y if spawner_room and not spawner_room.is_empty() else "none", ")") + if spawner_room and not spawner_room.is_empty(): + # Compare rooms by position and size + if spawner_room.x == room.x and spawner_room.y == room.y and \ + spawner_room.w == room.w and spawner_room.h == room.h: + spawner_in_room = true + print("RoomTrigger: ✓ Spawner room matches this trigger room!") + else: + print("RoomTrigger: ✗ Spawner room doesn't match - spawner: (", spawner_room.x, ",", spawner_room.y, ",", spawner_room.w, "x", spawner_room.h, "), trigger: (", room.x, ",", room.y, ",", room.w, "x", room.h, ")") + + # Also check blocking_room metadata (fallback) + if not spawner_in_room and child.has_meta("blocking_room"): + var blocking_room = child.get_meta("blocking_room") + print("RoomTrigger: Spawner has blocking_room metadata: (", blocking_room.x if blocking_room and not blocking_room.is_empty() else "none", ", ", blocking_room.y if blocking_room and not blocking_room.is_empty() else "none", ")") + if blocking_room and not blocking_room.is_empty(): + # Compare rooms by position and size + if blocking_room.x == room.x and blocking_room.y == room.y and \ + blocking_room.w == room.w and blocking_room.h == room.h: + spawner_in_room = true + print("RoomTrigger: ✓ Spawner blocking_room matches this trigger room!") + else: + print("RoomTrigger: ✗ Spawner blocking_room doesn't match - spawner: (", blocking_room.x, ",", blocking_room.y, ",", blocking_room.w, "x", blocking_room.h, "), trigger: (", room.x, ",", room.y, ",", room.w, "x", room.h, ")") + + # Also check by position (fallback if no room metadata) + if not spawner_in_room: + var tile_size = 16 + var spawner_tile_x = int(child.global_position.x / tile_size) + var spawner_tile_y = int(child.global_position.y / tile_size) + var room_min_x = room.x + 2 + var room_max_x = room.x + room.w - 2 + var room_min_y = room.y + 2 + var room_max_y = room.y + room.h - 2 + + print("RoomTrigger: Checking by position: spawner at tile (", spawner_tile_x, ", ", spawner_tile_y, "), room bounds: (", room_min_x, "-", room_max_x, ", ", room_min_y, "-", room_max_y, ")") + + if spawner_tile_x >= room_min_x and spawner_tile_x < room_max_x and \ + spawner_tile_y >= room_min_y and spawner_tile_y < room_max_y: + spawner_in_room = true + print("RoomTrigger: Spawner is within room bounds!") + + if spawner_in_room and not child in enemy_spawners: + enemy_spawners.append(child) + print("RoomTrigger: ✓ Added spawner ", child.name, " to this room trigger") + elif spawner_in_room: + print("RoomTrigger: Spawner already in list, skipping") + else: + print("RoomTrigger: Spawner is NOT in this room, skipping") + + print("RoomTrigger: Total spawners found: ", found_spawners_count, ", spawners in this room: ", enemy_spawners.size()) + + # Update debug label after finding entities + call_deferred("_update_debug_label") + +func _create_debug_label(): + # Create a debug label to show puzzle type and trigger status above the room + var label = Label.new() + label.name = "DebugLabel" + + # Position label at center-top of the room (relative to Area2D) + var tile_size = 16 + var room_world_w = (room.w - 4) * tile_size + + # Position at center of room, slightly above top edge + label.position = Vector2(room_world_w / 2.0 - 100, -40) # Centered horizontally, 40px above + label.size = Vector2(200, 50) # Wide and tall enough for text + + # Set high z-index to be above everything + label.z_index = 1000 + + # Enable autowrap so text doesn't overflow + label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + + # Load standard_font.png as bitmap font (it's imported as FontFile resource) + # The PNG is automatically imported as a FontFile when using font_data_image importer + # Use ResourceLoader.load() to ensure we get the FontFile resource + var standard_font_resource = ResourceLoader.load("res://assets/fonts/standard_font.png", "FontFile") + if standard_font_resource and standard_font_resource is FontFile: + label.add_theme_font_override("font", standard_font_resource) + label.add_theme_font_size_override("font_size", 8) + else: + # Fallback: just set smaller font size if font can't be loaded + label.add_theme_font_size_override("font_size", 8) + if not standard_font_resource: + print("RoomTrigger: WARNING - Could not load standard_font.png font resource!") + + # Style the label + label.add_theme_color_override("font_color", Color.YELLOW) + label.add_theme_color_override("font_shadow_color", Color.BLACK) + label.add_theme_constant_override("shadow_offset_x", 1) + label.add_theme_constant_override("shadow_offset_y", 1) + label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + label.vertical_alignment = VERTICAL_ALIGNMENT_TOP + + # Set initial text + label.text = "Room (%d,%d)\nPuzzle: ...\nStatus: Not entered" % [room.x, room.y] + + debug_label = label + add_child(label) + + # Update label after entities are found (deferred) + call_deferred("_update_debug_label") + +func _update_debug_label(): + # Update the debug label with current puzzle type and trigger status + if not debug_label: + return + + # Determine puzzle type from doors in this room + var puzzle_type = "none" + var puzzle_info = "" + + # Check doors to determine puzzle type + for door in doors_in_room: + if not is_instance_valid(door): + continue + + # Check if door is a blocking door + if door.has_method("_on_room_entered"): + if "type" in door: + if door.type == "KeyDoor": + puzzle_type = "keydoor" + puzzle_info = "KeyDoor" + break # KeyDoor takes priority + elif door.type == "StoneDoor" or door.type == "GateDoor": + # Check what puzzle type this door requires + # These are regular properties defined in door.gd (lines 35-36), so they should always exist + # Access properties directly - they're defined as var in the class + var door_requires_enemies = false + var door_requires_switch = false + + # Access properties directly (they're defined in door.gd as var requires_enemies and requires_switch) + # Since they're class properties, they should always exist, so just access them + door_requires_enemies = door.requires_enemies + door_requires_switch = door.requires_switch + + if door_requires_enemies: + puzzle_type = "enemy" + puzzle_info = "Enemy Spawner" + elif door_requires_switch: + # Check switch type by looking at connected switches + var has_pillar_switch = false + var has_walk_switch = false + for switch in floor_switches_in_room: + if is_instance_valid(switch): + # switch_type is an exported property in floor_switch.gd, so it should always exist + var switch_type_val = switch.switch_type + if switch_type_val == "pillar": + has_pillar_switch = true + elif switch_type_val == "walk": + has_walk_switch = true + + if has_pillar_switch: + puzzle_type = "switch_pillar" + puzzle_info = "Pillar Switch" + elif has_walk_switch: + puzzle_type = "switch_walk" + puzzle_info = "Walk Switch" + else: + puzzle_type = "switch" + puzzle_info = "Switch" + else: + # Door exists but no puzzle info - show door type + if puzzle_type == "none": + puzzle_type = door.type.to_lower() + puzzle_info = door.type + + # Also check switches and spawners directly + if floor_switches_in_room.size() > 0: + for switch in floor_switches_in_room: + if is_instance_valid(switch): + # switch_type is an exported property in floor_switch.gd, so it should always exist + var switch_type_val = switch.switch_type + if switch_type_val == "pillar": + puzzle_type = "switch_pillar" + puzzle_info = "Pillar Switch" + break + elif switch_type_val == "walk": + if puzzle_type != "switch_pillar": # Pillar takes priority + puzzle_type = "switch_walk" + puzzle_info = "Walk Switch" + + if enemy_spawners.size() > 0: + if puzzle_type != "keydoor": # KeyDoor takes priority + puzzle_type = "enemy" + puzzle_info = "Enemy Spawner (%d)" % enemy_spawners.size() + + # Determine status + var status_text = "ENTERED" if room_entered else "Not entered" + if enemies_spawned: + status_text += " | Spawned" + + # Update label text + debug_label.text = "Room (%d,%d)\nPuzzle: %s\nStatus: %s" % [room.x, room.y, puzzle_info, status_text] + + # Color code based on puzzle type + var color = Color.WHITE + match puzzle_type: + "enemy": + color = Color.RED + "switch_walk": + color = Color.GREEN + "switch_pillar": + color = Color.CYAN + "keydoor": + color = Color.YELLOW + _: + color = Color.GRAY + + if room_entered: + color = color.lerp(Color.WHITE, 0.3) # Make entered rooms brighter + + debug_label.add_theme_color_override("font_color", color) + + # Update label position to stay at center-top of room + var tile_size = 16 + var room_world_w = (room.w - 4) * tile_size + debug_label.position = Vector2(room_world_w / 2.0 - 100, -40) + +func check_puzzle_state(): + # Check if room puzzle is solved (all enemies defeated or switches activated) + # This is called by doors to check their state + var all_enemies_defeated = true + for enemy in enemies_in_room: + if is_instance_valid(enemy) and not enemy.is_dead: + all_enemies_defeated = false + break + + var all_switches_activated = true + for switch in floor_switches_in_room: + if is_instance_valid(switch): + # is_activated is a variable, not a method + if not switch.is_activated: + all_switches_activated = false + break + + return all_enemies_defeated or all_switches_activated diff --git a/src/scripts/room_trigger.gd.uid b/src/scripts/room_trigger.gd.uid new file mode 100644 index 0000000..8bb0a84 --- /dev/null +++ b/src/scripts/room_trigger.gd.uid @@ -0,0 +1 @@ +uid://mn3ighwoy0hi diff --git a/src/scripts/smoke_puff.gd b/src/scripts/smoke_puff.gd index 76c4c9a..18911bc 100644 --- a/src/scripts/smoke_puff.gd +++ b/src/scripts/smoke_puff.gd @@ -1,9 +1,11 @@ extends Node2D -# Smoke Puff Effect - Plays animation and fades out +# Smoke Puff Effect - Plays animation and fades out while moving slowly @export var animation_speed: float = 10.0 @export var fade_duration: float = 0.5 +@export var move_speed: float = 15.0 # Pixels per second movement speed +@export var move_duration: float = 1.5 # How long to move before starting fade @onready var sprite: Sprite2D = $Sprite2D @@ -11,31 +13,89 @@ var current_frame: int = 0 var frame_timer: float = 0.0 var total_frames: int = 4 # 4 frames per row var puff_type: int = 0 # 0 or 1 for first or second row +var move_direction: Vector2 = Vector2.ZERO # Direction to move in func _ready(): + # Add to group for easy cleanup + add_to_group("smoke_puff") + + # Wait for sprite to be ready (ensure @onready variable is set) + await get_tree().process_frame + + # Verify sprite exists + if not sprite: + push_error("SmokePuff: ERROR - Sprite2D not found! Check that scene has Sprite2D child node.") + queue_free() + return + # Randomly choose puff type puff_type = randi() % 2 + # Randomly choose movement direction (random angle, slow movement) + var random_angle = randf() * TAU # 0 to 2*PI + move_direction = Vector2(cos(random_angle), sin(random_angle)) + # Set initial frame sprite.frame = puff_type * total_frames current_frame = 0 + print("SmokePuff: Starting animation, sprite: ", sprite, ", frame: ", sprite.frame, ", move_direction: ", move_direction) + # Start animation animate_puff() func animate_puff(): - # Animate through the 4 frames - var tween = create_tween() + # Verify sprite still exists + if not sprite: + push_error("SmokePuff: ERROR - Sprite is null during animation!") + queue_free() + return - for i in range(total_frames): - tween.tween_callback(func(): - sprite.frame = puff_type * total_frames + i - ) - tween.tween_interval(1.0 / animation_speed) + # Calculate frame animation timing + var frame_interval = 1.0 / animation_speed # Time per frame + var frame_animation_duration = float(total_frames) * frame_interval + + # Set initial frame + sprite.frame = puff_type * total_frames + current_frame = 0 + frame_timer = 0.0 + + # Start movement tween + var move_distance = move_speed * move_duration + var target_position = global_position + move_direction * move_distance + var move_tween = create_tween() + if move_tween: + move_tween.tween_property(self, "global_position", target_position, move_duration) + + # After animation completes, fade out and remove + var total_animation_time = max(frame_animation_duration, move_duration) + await get_tree().create_timer(total_animation_time).timeout # Fade out - tween.tween_property(sprite, "modulate:a", 0.0, fade_duration) - - # Remove after animation - tween.tween_callback(queue_free) + if sprite: + print("SmokePuff: Starting fade out...") + var fade_tween = create_tween() + if fade_tween: + fade_tween.tween_property(sprite, "modulate:a", 0.0, fade_duration) + await fade_tween.finished + + print("SmokePuff: Animation complete, removing...") + queue_free() +func _process(delta): + # Handle frame animation in _process() for more reliable timing + if not sprite: + return + + # Update frame animation + if current_frame < total_frames: + frame_timer += delta + var frame_interval = 1.0 / animation_speed + + if frame_timer >= frame_interval: + frame_timer = 0.0 + current_frame += 1 + + if current_frame < total_frames: + sprite.frame = puff_type * total_frames + current_frame + print("SmokePuff: Frame ", current_frame, " -> ", sprite.frame) diff --git a/src/scripts/sword_projectile.gd b/src/scripts/sword_projectile.gd index 7cb8a6d..7269fbd 100644 --- a/src/scripts/sword_projectile.gd +++ b/src/scripts/sword_projectile.gd @@ -114,7 +114,13 @@ func _on_body_entered(body): else: # Fallback: broadcast if we can't get peer_id body.rpc_take_damage.rpc(damage, attacker_pos) - print("Sword projectile hit enemy: ", body.name, " for ", damage, " damage! (owner: ", player_owner.name if player_owner else "none", " is_authority: ", player_owner.is_multiplayer_authority() if player_owner else false, ")") + # Debug print - handle null player_owner safely + var owner_name: String = "none" + var is_authority: bool = false + if player_owner: + owner_name = str(player_owner.name) + is_authority = player_owner.is_multiplayer_authority() + print("Sword projectile hit enemy: ", body.name, " for ", damage, " damage! (owner: ", owner_name, " is_authority: ", is_authority, ")") return # Don't apply generic knockback, take_damage handles it # Deal damage to boxes or other damageable objects diff --git a/src/scripts/teleporter_into_closed_room.gd b/src/scripts/teleporter_into_closed_room.gd new file mode 100644 index 0000000..057a4de --- /dev/null +++ b/src/scripts/teleporter_into_closed_room.gd @@ -0,0 +1,34 @@ +extends Node2D + +@export var is_enabled = true # set to disabled for keydoors! + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + pass # Replace with function body. + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +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... + get_parent().teleportPlayer(body) + pass # Replace with function body. + + +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: + if !is_enabled: + return + $GPUParticles2D.emitting = false + pass # Replace with function body. diff --git a/src/scripts/teleporter_into_closed_room.gd.uid b/src/scripts/teleporter_into_closed_room.gd.uid new file mode 100644 index 0000000..f77c9ef --- /dev/null +++ b/src/scripts/teleporter_into_closed_room.gd.uid @@ -0,0 +1 @@ +uid://b4wejvn0dfrji