extends StaticBody2D @export_enum("GateDoor", "KeyDoor", "StoneDoor") var type: String = "StoneDoor" @export_enum("Left", "Up", "Right", "Down") var direction: String = "Up" #KeyDoors should always be closed at start #StoneDoor and GateDoor CAN be opened in start, and become closed when entering it's room #Then you must press a switch in the room or maybe you need to defeat all enemies in the room @export var is_closed: bool = true var is_closing:bool = false var is_opening:bool = false var time_to_move:float = 0.2 var move_timer:float = 0.0 var animation_start_position: Vector2 = Vector2.ZERO # Position when animation started 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 # Smoke puff scene for StoneDoor effects var smoke_puff_scene = preload("res://scenes/smoke_puff.tscn") # Called when the node enters the scene tree for the first time. func _ready() -> void: # Set texture based on door type _update_door_texture() # Rotate door first based on direction (original order) if direction == "Left": self.rotate(-PI/2) elif direction == "Right": self.rotate(PI/2) elif direction == "Down": self.rotate(PI) # 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": # 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": # 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": # 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": # 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 # 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") # Ensure door_index meta is set if name follows BlockingDoor_ if name.begins_with("BlockingDoor_") and not has_meta("door_index"): var index_str = name.substr(13) if index_str.is_valid_int(): set_meta("door_index", index_str.to_int()) 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 LogManager.log("Door: Set KeyDoor texture to door_locked.png", LogManager.CATEGORY_DOOR) else: LogManager.log_error("Door: Could not load door_locked.png texture!", LogManager.CATEGORY_DOOR) "GateDoor": var gate_texture = load("res://assets/gfx/door_gate.png") if gate_texture: sprite.texture = gate_texture LogManager.log("Door: Set GateDoor texture to door_gate.png", LogManager.CATEGORY_DOOR) else: LogManager.log_error("Door: Could not load door_gate.png texture!", LogManager.CATEGORY_DOOR) "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 LogManager.log("Door: Set StoneDoor texture to door_barred.png", LogManager.CATEGORY_DOOR) else: LogManager.log_error("Door: Could not load door_barred.png texture!", LogManager.CATEGORY_DOOR) # Called every frame. 'delta' is the elapsed time since the previous frame. func _process(delta: float) -> void: # Handle door opening/closing animation if is_opening or is_closing: # Safety check: ensure closed_position is valid before animating if closed_position == Vector2.ZERO: LogManager.log_error("Door: ERROR - closed_position is zero during animation! Resetting...", LogManager.CATEGORY_DOOR) closed_position = position - open_offset if is_opening else position is_opening = false is_closing = false 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 LogManager.log("Door: KeyDoor opening animation - start: " + str(start_pos) + ", target: " + str(target_pos) + ", offset: " + str(open_offset) + ", direction: " + str(direction), LogManager.CATEGORY_DOOR) # 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) var key_used_str = " (key_used=" + str(key_used) + ")" if type == "KeyDoor" else "" LogManager.log("Door: Opening animation complete - moved to open position: " + str(open_position) + " (closed: " + str(closed_position) + ", offset: " + str(open_offset) + ") - collision DISABLED" + key_used_str, LogManager.CATEGORY_DOOR) # CRITICAL: For KeyDoors, ensure key_used is true after animation completes # This prevents the door from being reset to closed in _process() if type == "KeyDoor": key_used = true # Spawn smoke puffs when StoneDoor finishes opening (1-2 puffs) if type == "StoneDoor": _spawn_smoke_puffs_on_open() # Animation finished, reset flags is_opening = false is_closing = false 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) LogManager.log("Door: Closing animation complete - moved to closed position: " + str(closed_position) + " - collision ENABLED", LogManager.CATEGORY_DOOR) # Spawn smoke puffs when StoneDoor finishes closing (1-3 puffs) if type == "StoneDoor": _spawn_smoke_puffs_on_close() # Animation finished, reset flags is_opening = false is_closing = false 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: LogManager.log("Door: KeyDoor was moved incorrectly! Resetting to closed position.", LogManager.CATEGORY_DOOR) 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(): # Only open on server/authority in multiplayer, then sync to clients if multiplayer.has_multiplayer_peer() and not multiplayer.is_server(): return # Clients wait for RPC $TeleporterIntoClosedRoom.is_enabled = false # CRITICAL: For KeyDoors, ensure they start from closed position before opening # KeyDoors should ALWAYS start from closed position when opening (never from open position) 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 LogManager.log("Door: KeyDoor _open() called - reset to closed position " + str(closed_position) + " before opening", LogManager.CATEGORY_DOOR) else: LogManager.log_error("Door: KeyDoor _open() called but closed_position is zero!", LogManager.CATEGORY_DOOR) 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 LogManager.log("Door: _open() called but door is already open! Position: " + str(position) + ", closed: " + str(closed_position) + ", distance: " + str(distance_to_closed), LogManager.CATEGORY_DOOR) # 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) LogManager.log("Door: StoneDoor/GateDoor _open() called - ensuring door is at closed position " + str(closed_position) + " before opening", LogManager.CATEGORY_DOOR) else: LogManager.log_error("Door: StoneDoor/GateDoor _open() called but closed_position is zero!", LogManager.CATEGORY_DOOR) return if type == "GateDoor": $SfxOpenGateDoor.play() else: $SfxOpenStoneDoor.play() # CRITICAL: Store starting position for animation (should be closed_position) animation_start_position = position LogManager.log("Door: Starting open animation from " + str(animation_start_position) + " to " + str(closed_position + open_offset) + " (offset: " + str(open_offset) + ")", LogManager.CATEGORY_DOOR) is_opening = true is_closing = false move_timer = 0.0 # Sync door opening to clients in multiplayer if multiplayer.has_multiplayer_peer() and multiplayer.is_server() and is_inside_tree(): var game_world = get_tree().get_first_node_in_group("game_world") if game_world and game_world.has_method("_rpc_to_ready_peers"): game_world._rpc_to_ready_peers("_sync_door_open_by_name", [name]) # Also sync puzzle_solved state game_world._rpc_to_ready_peers("_sync_door_puzzle_solved_by_name", [name, puzzle_solved]) # Track door state for syncing to new clients if game_world: game_world.door_states[name] = { "is_closed": false, "puzzle_solved": puzzle_solved, "key_used": key_used if "key_used" in self else false, "position": position, "closed_position": closed_position, "open_offset": open_offset } func _close(): # Only close on server/authority in multiplayer, then sync to clients if multiplayer.has_multiplayer_peer() and not multiplayer.is_server(): return # Clients wait for RPC # CRITICAL: KeyDoors should NEVER be closed (they only open with a key and stay open) if type == "KeyDoor": LogManager.log_error("Door: ERROR - _close() called on KeyDoor! KeyDoors should never be closed!", LogManager.CATEGORY_DOOR) 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 LogManager.log("Door: WARNING - closed_position was zero, using current position: " + str(closed_position), LogManager.CATEGORY_DOOR) # 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 LogManager.log("Door: _close() called - is_closed: " + str(is_closed) + ", is_actually_at_closed: " + str(is_actually_at_closed) + ", position: " + str(position) + ", closed: " + str(closed_position) + ", distance: " + str(distance_to_closed), LogManager.CATEGORY_DOOR) # 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: LogManager.log("Door: Already closed (both flag and position match), not closing again", LogManager.CATEGORY_DOOR) 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 LogManager.log("Door: Door is at closed position but flag says open! Fixing state only (no animation)", LogManager.CATEGORY_DOOR) 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 LogManager.log("Door: WARNING - Door is far from expected open position! Resetting to open: " + str(expected_open_pos) + " (was at: " + str(position) + ", distance: " + str(distance_to_open) + ")", LogManager.CATEGORY_DOOR) 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 LogManager.log("Door: Starting close animation from " + str(animation_start_position) + " to " + str(closed_position) + " (offset: " + str(open_offset) + ")", LogManager.CATEGORY_DOOR) if type == "GateDoor": $SfxCloseGateDoor.play() else: $SfxDoorCloses.play() is_opening = false is_closing = true move_timer = 0.0 $TeleporterIntoClosedRoom.is_enabled = true # Sync door closing to clients in multiplayer if multiplayer.has_multiplayer_peer() and multiplayer.is_server() and is_inside_tree(): var game_world = get_tree().get_first_node_in_group("game_world") if game_world and game_world.has_method("_rpc_to_ready_peers"): game_world._rpc_to_ready_peers("_sync_door_close_by_name", [name]) # Track door state for syncing to new clients if game_world: game_world.door_states[name] = { "is_closed": true, "puzzle_solved": puzzle_solved, "key_used": key_used if "key_used" in self else false, "position": position, "closed_position": closed_position, "open_offset": open_offset } 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) LogManager.log("Door: _ready_after_setup() called - type: " + str(type) + ", direction: " + str(direction) + ", is_closed: " + str(is_closed) + ", open_position: " + str(open_position), LogManager.CATEGORY_DOOR) # 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 LogManager.log("Door: Calculated positions - open: " + str(open_position) + ", closed: " + str(closed_position) + ", closed_offset: " + str(closed_offset) + ", open_offset: " + str(open_offset), LogManager.CATEGORY_DOOR) # 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 LogManager.log("Door: KeyDoor starting CLOSED at position " + str(position) + " (direction: " + str(direction) + "), will open to " + str(closed_position + open_offset) + " - collision ENABLED", LogManager.CATEGORY_DOOR) # 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) LogManager.log("Door: Starting CLOSED at position " + str(position) + " (type: " + str(type) + ", direction: " + str(direction) + ") - collision ENABLED", LogManager.CATEGORY_DOOR) 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 LogManager.log("Door: WARNING - Position doesn't match open_position! Forcing to open: " + str(open_position) + " (was: " + str(position) + ")", LogManager.CATEGORY_DOOR) 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 LogManager.log("Door: Starting OPEN at position " + str(position) + " (closed: " + str(closed_position) + ", open: " + str(open_position) + ", open_offset: " + str(open_offset) + ", type: " + str(type) + ", direction: " + str(direction) + ") - collision DISABLED, is_closed: " + str(is_closed), LogManager.CATEGORY_DOOR) # 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: LogManager.log_error("Door: ERROR - Door open/closed distance is wrong! Position: " + str(position) + ", closed: " + str(closed_position) + ", distance: " + str(actual_distance) + " (expected: " + str(expected_distance) + ")", LogManager.CATEGORY_DOOR) # 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) LogManager.log("Door: FORCED door to open position: " + str(position) + " (distance to closed: " + str(position.distance_to(closed_position)) + ", is_closed: " + str(is_closed) + ")", LogManager.CATEGORY_DOOR) # 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: LogManager.log_error("Door: ERROR - Door is at open position but is_closed is true! Fixing state...", LogManager.CATEGORY_DOOR) is_closed = false set_collision_layer_value(7, false) LogManager.log("Door: Fixed state - door is now OPEN (is_closed: " + str(is_closed) + ", collision: " + str(get_collision_layer_value(7)) + ")", LogManager.CATEGORY_DOOR) elif not should_be_open and not is_closed: LogManager.log_error("Door: ERROR - Door is at closed position but is_closed is false! Fixing state...", LogManager.CATEGORY_DOOR) is_closed = true set_collision_layer_value(7, true) LogManager.log("Door: Fixed state - door is now CLOSED (is_closed: " + str(is_closed) + ", collision: " + str(get_collision_layer_value(7)) + ")", LogManager.CATEGORY_DOOR) # 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 LogManager.log("Door: _on_room_entered() - type: " + str(type) + ", is_closed: " + str(is_closed) + ", is_actually_open: " + str(is_actually_open) + ", position: " + str(position) + ", closed: " + str(closed_position) + ", distance: " + str(distance_to_closed), LogManager.CATEGORY_DOOR) # 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 LogManager.log("Door: Closing door on room entry - was at position " + str(position) + " (closed: " + str(closed_position) + ", is_closed: " + str(is_closed) + ", distance: " + str(distance_to_closed) + ")", LogManager.CATEGORY_DOOR) # 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 LogManager.log("Door: WARNING - Door is not at expected open position! Resetting to open: " + str(expected_open_pos) + " (was at: " + str(position) + ")", LogManager.CATEGORY_DOOR) 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 LogManager.log("Door: Door is open but animation in progress, not closing", LogManager.CATEGORY_DOOR) 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 LogManager.log("Door: WARNING - Door is already at closed position when entering room! This shouldn't happen for StoneDoor/GateDoor that start open.", LogManager.CATEGORY_DOOR) 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 LogManager.log("Door: Door was already closed - ensuring state is correct, position: " + str(position) + ", closed: " + str(closed_position), LogManager.CATEGORY_DOOR) # 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(): # Only check puzzle state on server/authority in multiplayer if multiplayer.has_multiplayer_peer() and not multiplayer.is_server(): return # Clients wait for server to check and sync via RPC # CRITICAL: Don't check puzzle state while door is animating (closing or opening) # This prevents race conditions where switch triggers before door finishes closing if is_closing or is_opening: LogManager.log("Door: Skipping puzzle check - door is animating (is_closing: " + str(is_closing) + ", is_opening: " + str(is_opening) + ")", LogManager.CATEGORY_DOOR) return # Check door's actual state (position-based check is more reliable than flags) var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0 var is_actually_closed = distance_to_closed < 5.0 # Within 5 pixels of closed position var is_actually_open = distance_to_closed > 5.0 # More than 5 pixels away from closed position var collision_enabled = get_collision_layer_value(7) # Check if collision layer 7 is enabled # CRITICAL: If puzzle_solved is true but door is not actually open (not in open position or collision still enabled), # allow switch to trigger again to open the door # This handles race conditions where switch triggers while door is still closing if puzzle_solved and (not is_actually_open or collision_enabled): # Door should be open but isn't (position or collision) - reset puzzle_solved to allow switch to trigger again LogManager.log("Door: puzzle_solved is true but door is not actually open (position: " + str(is_actually_open) + ", collision: " + str(collision_enabled) + ") - resetting to allow switch to trigger again", LogManager.CATEGORY_DOOR) puzzle_solved = false switches_activated = false # Check if all enemies are defeated (enemies in blocking room) if requires_enemies and _are_all_enemies_defeated(): var room_x_str = str(blocking_room.get("x", "?")) if blocking_room and not blocking_room.is_empty() else "?" var room_y_str = str(blocking_room.get("y", "?")) if blocking_room and not blocking_room.is_empty() else "?" LogManager.log("Door: All enemies defeated! Opening door " + str(name) + " (type: " + str(type) + ", room: " + room_x_str + "," + room_y_str + ")", LogManager.CATEGORY_DOOR) enemies_defeated = true puzzle_solved = true if is_actually_closed: _open() return # Check if all required switches are activated (switches in switch_room, before the door) var all_switches_active = _are_all_switches_activated() # Check if any connected switches are pillar switches (for special handling) var has_pillar_switch = false for switch in connected_switches: if is_instance_valid(switch) and "switch_type" in switch and switch.switch_type == "pillar": has_pillar_switch = true break if all_switches_active: # All switches are active - solve puzzle and open door if closed switches_activated = true puzzle_solved = true # Only open if door is actually closed (not just the flag, but actual position) # This prevents race condition where switch triggers while door is still closing if is_actually_closed: _open() return else: # Not all switches are active if puzzle_solved and has_pillar_switch: # Pillar switch became inactive and door was open - close it and reset puzzle LogManager.log("Door: Pillar switch deactivated - closing door " + str(name), LogManager.CATEGORY_DOOR) switches_activated = false puzzle_solved = false if not is_actually_closed: _close() return elif puzzle_solved and not has_pillar_switch: # Walk switch puzzle - once solved, stays solved (door stays open) # Don't reset puzzle_solved for walk switches return else: # Puzzle not solved yet - just reset flags switches_activated = false puzzle_solved = false func _are_all_enemies_defeated() -> bool: # Check if all enemies spawned from spawners in the puzzle room are defeated # 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 game_world = get_tree().get_first_node_in_group("game_world") var entities_node = null if game_world: entities_node = game_world.get_node_or_null("Entities") if not entities_node: # Fallback without throwing if GameWorld isn't ready yet entities_node = get_node_or_null("/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 # Use tile_size and room bounds from parent scope (declared below) var enemy_tile_x = int(child.global_position.x / 16) var enemy_tile_y = int(child.global_position.y / 16) var enemy_room_min_x = target_room.x + 2 var enemy_room_max_x = target_room.x + target_room.w - 2 var enemy_room_min_y = target_room.y + 2 var enemy_room_max_y = target_room.y + target_room.h - 2 if enemy_tile_x >= enemy_room_min_x and enemy_tile_x < enemy_room_max_x and \ enemy_tile_y >= enemy_room_min_y and enemy_tile_y < enemy_room_max_y: enemy_in_room = true # Also check spawner metadata - if enemy has spawner_name matching this room's spawners if child.has_meta("spawner_name"): var spawner_name = child.get_meta("spawner_name") # Spawner names are like "EnemySpawner__" if str(target_room.x) in spawner_name and str(target_room.y) in spawner_name: enemy_in_room = true # Confirmed by spawner name if enemy_in_room: room_spawned_enemies.append(child) var spawner_name = str(child.get_meta("spawner_name")) if child.has_meta("spawner_name") else "unknown" var is_dead_str = str(child.is_dead) if "is_dead" in child else "unknown" LogManager.log("Door: Found spawned enemy in room: " + str(child.name) + " (spawner: " + spawner_name + ", is_dead: " + is_dead_str + ")", LogManager.CATEGORY_DOOR) # Check if all spawned enemies are dead var target_room_x_str = str(target_room.get("x", "?")) if target_room and not target_room.is_empty() else "?" var target_room_y_str = str(target_room.get("y", "?")) if target_room and not target_room.is_empty() else "?" LogManager.log("Door: _are_all_enemies_defeated() - Found " + str(room_spawned_enemies.size()) + " spawned enemies in room (" + target_room_x_str + "," + target_room_y_str + ")", LogManager.CATEGORY_DOOR) # First, check if any enemies in room_spawned_enemies are still alive # If any are alive, puzzle is not solved for enemy in room_spawned_enemies: if is_instance_valid(enemy): var enemy_is_dead = false if "is_dead" in enemy: enemy_is_dead = enemy.is_dead else: enemy_is_dead = enemy.is_queued_for_deletion() or not enemy.is_inside_tree() if not enemy_is_dead: LogManager.log("Door: Enemy " + str(enemy.name) + " is still alive - puzzle not solved yet", LogManager.CATEGORY_DOOR) return false # Enemy is still alive, puzzle not solved # If we have enemies and all are dead, puzzle is solved if room_spawned_enemies.size() > 0: LogManager.log("Door: All " + str(room_spawned_enemies.size()) + " spawned enemies are dead! Puzzle solved!", LogManager.CATEGORY_DOOR) return true # All enemies found are dead # No spawned enemies found - check if spawners have actually spawned enemies before # CRITICAL: Only consider puzzle solved if spawners have spawned enemies and they're all dead # Don't solve if spawners haven't spawned yet (e.g., spawn_on_ready=false and player hasn't entered room) # IMPORTANT: Before checking spawners, verify there are NO ALIVE enemies in the room # This catches cases where enemies weren't added to room_spawned_enemies due to position check issues var entities_child = entities_node.get_node_or_null("Entities") if entities_node else null if not entities_child and entities_node: var fallback_game_world = get_tree().get_first_node_in_group("game_world") if fallback_game_world: entities_child = fallback_game_world.get_node_or_null("Entities") var tile_size = 16 var room_min_x = target_room.x + 2 var room_max_x = target_room.x + target_room.w - 2 var room_min_y = target_room.y + 2 var room_max_y = target_room.y + target_room.h - 2 if entities_child: for child in entities_child.get_children(): if child.is_in_group("enemy"): # Only check enemies that were spawned from spawners if not child.has_meta("spawned_from_spawner") or not child.get_meta("spawned_from_spawner"): continue # Check if enemy is in this room by position var enemy_tile_x = int(child.global_position.x / tile_size) var enemy_tile_y = int(child.global_position.y / tile_size) var enemy_in_room = (enemy_tile_x >= room_min_x and enemy_tile_x < room_max_x and \ enemy_tile_y >= room_min_y and enemy_tile_y < room_max_y) if not enemy_in_room: continue # Skip enemies not in this room # Check if enemy is alive var enemy_is_alive = false if "is_dead" in child: enemy_is_alive = not child.is_dead else: enemy_is_alive = not child.is_queued_for_deletion() and child.is_inside_tree() if enemy_is_alive: # Found an ALIVE enemy in this room - puzzle not solved! LogManager.log("Door: Found ALIVE enemy " + str(child.name) + " in room - puzzle not solved yet (enemy still alive)", LogManager.CATEGORY_DOOR) return false # No alive enemies found in room - now check if spawners have spawned if room_spawned_enemies.size() == 0: # No spawned enemies found - check if spawners have actually spawned enemies before # CRITICAL: Only consider puzzle solved if spawners have spawned enemies and they're all dead # Don't solve if spawners haven't spawned yet (e.g., spawn_on_ready=false and player hasn't entered room) var spawners_in_room = [] var spawners_that_have_spawned = [] # First, check ALL spawners in the room (including destroyed ones by checking for enemies with their names) for spawner in get_tree().get_nodes_in_group("enemy_spawner"): if is_instance_valid(spawner): var spawner_tile_x = int(spawner.global_position.x / 16) var spawner_tile_y = int(spawner.global_position.y / 16) if spawner_tile_x >= target_room.x + 2 and spawner_tile_x < target_room.x + target_room.w - 2 and \ spawner_tile_y >= target_room.y + 2 and spawner_tile_y < target_room.y + target_room.h - 2: spawners_in_room.append(spawner) # Check if this spawner has spawned by multiple methods: # 1. Check if spawner has has_ever_spawned flag (most reliable) # 2. Check if any enemies in scene were spawned by this spawner # 3. Check if spawner has spawned_enemies list with valid enemies var has_spawned = false # First, check if spawner has has_ever_spawned flag if "has_ever_spawned" in spawner and spawner.has_ever_spawned: has_spawned = true # Also check if any enemies in scene were spawned by this spawner (ONLY if they're dead/removed) # If we find ALIVE enemies, don't mark spawner as spawned - wait until they're dead if not has_spawned: var entities_child_for_spawner = entities_node.get_node_or_null("Entities") if entities_node else null if not entities_child_for_spawner and entities_node: var fallback_game_world = get_tree().get_first_node_in_group("game_world") if fallback_game_world: entities_child_for_spawner = fallback_game_world.get_node_or_null("Entities") if entities_child_for_spawner: for child in entities_child_for_spawner.get_children(): if child.is_in_group("enemy"): if child.has_meta("spawner_name") and child.get_meta("spawner_name") == spawner.name: # Found an enemy spawned by this spawner # Check if it's alive - if alive, don't mark spawner as spawned yet var enemy_is_alive = false if "is_dead" in child: enemy_is_alive = not child.is_dead else: enemy_is_alive = not child.is_queued_for_deletion() and child.is_inside_tree() if not enemy_is_alive: # Enemy is dead - spawner has spawned has_spawned = true break # If enemy is alive, don't mark spawner as spawned - wait until it's dead # Also check if spawner currently has enemies in its spawned_enemies list # Only count if all enemies in the list are dead (puzzle can only be solved when all are dead) if not has_spawned and "spawned_enemies" in spawner: var spawned_enemies_list = spawner.spawned_enemies if spawned_enemies_list and spawned_enemies_list.size() > 0: # Check if all enemies in list are dead var all_enemies_dead = true for enemy in spawned_enemies_list: if is_instance_valid(enemy): var enemy_is_dead = false if "is_dead" in enemy: enemy_is_dead = enemy.is_dead else: enemy_is_dead = enemy.is_queued_for_deletion() or not enemy.is_inside_tree() if not enemy_is_dead: all_enemies_dead = false break # Only mark spawner as spawned if all enemies are dead if all_enemies_dead: has_spawned = true if has_spawned: spawners_that_have_spawned.append(spawner) # Also check for spawners that may have been destroyed but had enemies spawned # Look for any dead enemies with spawner name matching this room AND position in this room var spawner_name_pattern = "EnemySpawner_" + str(target_room.x) + "_" + str(target_room.y) var found_dead_enemies_with_matching_spawner = [] if entities_child: for child in entities_child.get_children(): if child.is_in_group("enemy"): # Only check enemies that were spawned from spawners if not child.has_meta("spawned_from_spawner") or not child.get_meta("spawned_from_spawner"): continue # Check if enemy is in this room by position var enemy_tile_x = int(child.global_position.x / tile_size) var enemy_tile_y = int(child.global_position.y / tile_size) var enemy_in_room = (enemy_tile_x >= room_min_x and enemy_tile_x < room_max_x and \ enemy_tile_y >= room_min_y and enemy_tile_y < room_max_y) if not enemy_in_room: continue # Skip enemies not in this room # Check if enemy is dead var enemy_is_dead = false if "is_dead" in child: enemy_is_dead = child.is_dead else: enemy_is_dead = child.is_queued_for_deletion() or not child.is_inside_tree() # Only check dead/removed enemies as evidence that spawners spawned if enemy_is_dead and child.has_meta("spawner_name"): var enemy_spawner_name = child.get_meta("spawner_name") if spawner_name_pattern in enemy_spawner_name or enemy_spawner_name == spawner_name_pattern: found_dead_enemies_with_matching_spawner.append(child) # Track unique spawner names that have spawned (only based on dead enemies) var unique_spawner_names_that_spawned = {} for enemy in found_dead_enemies_with_matching_spawner: if enemy.has_meta("spawner_name"): var spawner_name = enemy.get_meta("spawner_name") unique_spawner_names_that_spawned[spawner_name] = true # Only mark spawners as spawned if we found DEAD enemies (ensures puzzle only solves when all are dead) if unique_spawner_names_that_spawned.size() > 0: # Found dead enemies with matching spawner names - spawners definitely spawned and enemies are dead if spawners_in_room.size() == 0: for spawner_name in unique_spawner_names_that_spawned.keys(): spawners_in_room.append(null) # Placeholder for destroyed spawner spawners_that_have_spawned.append(null) # Count as spawned LogManager.log("Door: Spawner " + str(spawner_name) + " was destroyed but spawned enemies that are now all dead - counting as spawned", LogManager.CATEGORY_DOOR) else: # Spawners exist - check if any weren't counted as spawned yet for spawner_name in unique_spawner_names_that_spawned.keys(): var spawner_already_counted = false for spawned_spawner in spawners_that_have_spawned: if is_instance_valid(spawned_spawner) and spawned_spawner.name == spawner_name: spawner_already_counted = true break if not spawner_already_counted: for i in range(spawners_in_room.size()): var spawner = spawners_in_room[i] if is_instance_valid(spawner) and spawner.name == spawner_name: spawners_that_have_spawned.append(spawner) LogManager.log("Door: Found dead enemy from spawner " + str(spawner_name) + " - marking as spawned", LogManager.CATEGORY_DOOR) break # Only solve if: # 1. There are spawners in the room (or were spawners that spawned) # 2. All spawners have spawned enemies # 3. All those enemies are now dead (since no enemies found in room) # Count valid spawners (including null placeholders for destroyed spawners) var valid_spawners_count = spawners_in_room.size() var valid_spawned_count = spawners_that_have_spawned.size() # If we have spawners (including destroyed ones) and they've all spawned, puzzle is solved if valid_spawners_count > 0 and valid_spawned_count >= valid_spawners_count: # All spawners in room have spawned at least once, and no enemies found in room # This means all spawned enemies are dead - puzzle solved! LogManager.log("Door: No spawned enemies found, but all " + str(valid_spawners_count) + " spawners in room have spawned enemies that are all dead - puzzle solved!", LogManager.CATEGORY_DOOR) return true # Also check: if no spawners found (they were destroyed), but this is a puzzle room (has blocking doors), # and we previously found enemies with matching spawner names that are now gone, # consider the puzzle solved if valid_spawners_count == 0 and valid_spawned_count > 0: # Spawners were destroyed, but we found evidence they spawned # Since no enemies found, they must all be dead - puzzle solved! LogManager.log("Door: No spawners or enemies found, but found evidence of spawned enemies that are now all dead - puzzle solved!", LogManager.CATEGORY_DOOR) return true if valid_spawners_count > 0: LogManager.log("Door: Spawners in room (" + str(valid_spawners_count) + ") but only " + str(valid_spawned_count) + " have spawned - puzzle not solved yet", LogManager.CATEGORY_DOOR) else: LogManager.log("Door: No spawned enemies found in room - puzzle not solved yet (enemies may not have spawned or already removed)", LogManager.CATEGORY_DOOR) 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: LogManager.log("Door: Enemy " + str(enemy.name) + " is still alive (is_dead: " + str(enemy_is_dead) + ", is_queued: " + str(enemy.is_queued_for_deletion()) + ", in_tree: " + str(enemy.is_inside_tree()) + ")", LogManager.CATEGORY_DOOR) return false else: # Enemy is no longer valid (removed from scene) - consider it dead LogManager.log("Door: Enemy is no longer valid (removed from scene) - counting as dead", LogManager.CATEGORY_DOOR) LogManager.log("Door: All " + str(room_spawned_enemies.size()) + " spawned enemies are dead! Puzzle solved!", LogManager.CATEGORY_DOOR) return true # All enemies are dead func _spawn_smoke_puffs_on_close(): # Spawn 1-3 smoke puffs when StoneDoor finishes closing if not smoke_puff_scene: return var puff_count = randi_range(1, 3) # Random between 1-3 puffs for i in range(puff_count): var puff = smoke_puff_scene.instantiate() if puff: # Spawn at door position with small random offset var offset_x = randf_range(-8, 8) var offset_y = randf_range(-8, 8) puff.global_position = global_position + Vector2(offset_x, offset_y) puff.z_index = 10 # High z-index to ensure visibility # Add to Entities node for proper layering var entities_node = get_tree().get_first_node_in_group("game_world") if entities_node: entities_node = entities_node.get_node_or_null("Entities") if entities_node: entities_node.add_child(puff) else: # Fallback: add to scene root get_tree().current_scene.add_child(puff) else: # Fallback: add to scene root get_tree().current_scene.add_child(puff) func _spawn_smoke_puffs_on_open(): # Spawn 1-2 smoke puffs when StoneDoor starts opening if not smoke_puff_scene: return var puff_count = randi_range(1, 2) # Random between 1-2 puffs for i in range(puff_count): var puff = smoke_puff_scene.instantiate() if puff: # Spawn at door position with small random offset var offset_x = randf_range(-8, 8) var offset_y = randf_range(-8, 8) puff.global_position = global_position + Vector2(offset_x, offset_y) puff.z_index = 10 # High z-index to ensure visibility # Add to Entities node for proper layering var entities_node = get_tree().get_first_node_in_group("game_world") if entities_node: entities_node = entities_node.get_node_or_null("Entities") if entities_node: entities_node.add_child(puff) else: # Fallback: add to scene root get_tree().current_scene.add_child(puff) else: # Fallback: add to scene root get_tree().current_scene.add_child(puff) func _are_all_switches_activated() -> bool: # Check if all required switches are activated # CRITICAL: ONLY check connected_switches - switches are explicitly connected when spawned # 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) var switch_room_x = str(blocking_room.get("x", "?")) if blocking_room and not blocking_room.is_empty() else "?" var switch_room_y = str(blocking_room.get("y", "?")) if blocking_room and not blocking_room.is_empty() else "?" LogManager.log("Door: _are_all_switches_activated() - Checking " + str(connected_switches.size()) + " connected switches for door " + str(name) + " (room: " + switch_room_x + "," + switch_room_y + ")", LogManager.CATEGORY_DOOR) 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: LogManager.log("Door: Switch " + str(switch.name) + " is NOT activated", LogManager.CATEGORY_DOOR) return false LogManager.log("Door: All connected switches are activated!", LogManager.CATEGORY_DOOR) 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 LogManager.log("Door: WARNING - Door " + str(name) + " has no connected switches! Puzzle cannot be solved!", LogManager.CATEGORY_DOOR) 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() LogManager.log("KeyDoor opened with key!", LogManager.CATEGORY_DOOR) 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): # Only teleport on server in multiplayer (server is authority for all players) if multiplayer.has_multiplayer_peer() and not multiplayer.is_server(): # Client should wait for server to sync position return var keydoor_open_offset = Vector2.ZERO match direction: "Up": 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 var new_position = self.global_position + keydoor_open_offset if body.is_in_group("player"): # Player teleportation - set position on server and sync to all clients if multiplayer.has_multiplayer_peer() and multiplayer.is_server(): # Server: set position directly and sync to all clients body.global_position = new_position # Also reset velocity to prevent player from moving back if "velocity" in body: body.velocity = Vector2.ZERO # Sync position to all clients (including the teleported player's client) if body.has_method("_sync_teleport_position") and body.can_send_rpcs and body.is_inside_tree(): body._sync_teleport_position.rpc(new_position) else: # Single-player: just set position body.global_position = new_position if "velocity" in body: body.velocity = Vector2.ZERO else: # Non-player teleportation (shouldn't happen, but handle it) body.global_position = new_position pass @rpc("authority", "reliable") func _sync_door_open(): # Client receives door open RPC - open the door locally if multiplayer.has_multiplayer_peer() and not multiplayer.is_server(): # Only open if door is closed (avoid reopening already open doors) var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0 var is_actually_open = distance_to_closed > 5.0 if not is_actually_open and not is_opening: # Door is closed - open it if closed_position != Vector2.ZERO: position = closed_position global_position = closed_position is_closed = true set_collision_layer_value(7, true) # CRITICAL: For KeyDoors, set key_used to true so it doesn't get reset to closed if type == "KeyDoor": key_used = true animation_start_position = position is_opening = true is_closing = false move_timer = 0.0 # Play sound effect on client if type == "KeyDoor": $SfxOpenKeyDoor.play() elif type == "GateDoor": $SfxOpenGateDoor.play() else: $SfxOpenStoneDoor.play() var key_used_str = " (key_used=" + str(key_used) + ")" if type == "KeyDoor" else "" LogManager.log("Door: Client received door open RPC for " + str(name) + " - starting open animation" + key_used_str, LogManager.CATEGORY_DOOR) @rpc("authority", "reliable") func _sync_puzzle_solved(is_solved: bool): # Client receives puzzle solved state sync if multiplayer.has_multiplayer_peer() and not multiplayer.is_server(): puzzle_solved = is_solved if is_solved: enemies_defeated = true switches_activated = true LogManager.log("Door: Client received puzzle_solved sync for " + str(name) + " - puzzle_solved: " + str(is_solved), LogManager.CATEGORY_DOOR) @rpc("authority", "reliable") func _sync_door_close(): # Client receives door close RPC - close the door locally if multiplayer.has_multiplayer_peer() and not multiplayer.is_server(): # Only close if door is open (avoid reclosing already closed doors) var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0 var is_actually_at_closed = distance_to_closed < 5.0 if not is_actually_at_closed and not is_closing: # Door is open - close it var expected_open_pos = closed_position + open_offset var distance_to_open = position.distance_to(expected_open_pos) if distance_to_open > 10.0: animation_start_position = expected_open_pos position = expected_open_pos global_position = expected_open_pos else: animation_start_position = position is_opening = false is_closing = true move_timer = 0.0 LogManager.log("Door: Client received door close RPC for " + str(name) + " - starting close animation", LogManager.CATEGORY_DOOR)