added blocking doors to paths.

This commit is contained in:
2026-01-10 19:46:55 +01:00
parent 24ea2f3c60
commit 25be2c00bd
33 changed files with 4383 additions and 455 deletions

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -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"]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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_<room_x>_<room_y>"
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

File diff suppressed because it is too large Load Diff

View File

@@ -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:

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

256
src/scripts/floor_switch.gd Normal file
View File

@@ -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!")

View File

@@ -0,0 +1 @@
uid://dfigqc0flmid5

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

605
src/scripts/room_trigger.gd Normal file
View File

@@ -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

View File

@@ -0,0 +1 @@
uid://mn3ighwoy0hi

View File

@@ -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)

View File

@@ -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

View File

@@ -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.

View File

@@ -0,0 +1 @@
uid://b4wejvn0dfrji