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 0000000..5266bd0 Binary files /dev/null and b/src/assets/audio/sfx/environment/keydoor/door_closes.mp3 differ diff --git a/src/assets/audio/sfx/environment/keydoor/door_closes.mp3.import b/src/assets/audio/sfx/environment/keydoor/door_closes.mp3.import new file mode 100644 index 0000000..a6fc19d --- /dev/null +++ b/src/assets/audio/sfx/environment/keydoor/door_closes.mp3.import @@ -0,0 +1,19 @@ +[remap] + +importer="mp3" +type="AudioStreamMP3" +uid="uid://c6bp156a5ggdf" +path="res://.godot/imported/door_closes.mp3-b5c10c6d847d1c83f09bcec9f8b04f75.mp3str" + +[deps] + +source_file="res://assets/audio/sfx/environment/keydoor/door_closes.mp3" +dest_files=["res://.godot/imported/door_closes.mp3-b5c10c6d847d1c83f09bcec9f8b04f75.mp3str"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/src/assets/gfx/RPG DUNGEON VOL 3.png b/src/assets/gfx/RPG DUNGEON VOL 3.png index 2eb2e98..d520669 100644 Binary files a/src/assets/gfx/RPG DUNGEON VOL 3.png and b/src/assets/gfx/RPG DUNGEON VOL 3.png differ diff --git a/src/assets/gfx/pickups/items_n_shit.png b/src/assets/gfx/pickups/items_n_shit.png index 0e2d4e9..4294cbd 100644 Binary files a/src/assets/gfx/pickups/items_n_shit.png and b/src/assets/gfx/pickups/items_n_shit.png differ 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