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_<index>
	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_visibility():
	# Hide door sprite when open; show when closed. Re-evaluate on any state change.
	var sprite = get_node_or_null("Sprite2D")
	if sprite:
		sprite.visible = is_closed

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)
	
	_update_door_visibility()

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)
	
	_update_door_visibility()
	
	# 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_<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)
				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
			
			# Play close sound on clients (host plays it in _close())
			if type == "GateDoor":
				var sfx = get_node_or_null("SfxCloseGateDoor")
				if sfx and sfx.stream:
					sfx.play()
			else:
				var sfx = get_node_or_null("SfxDoorCloses")
				if sfx and sfx.stream:
					sfx.play()
			
			LogManager.log("Door: Client received door close RPC for " + str(name) + " - starting close animation", LogManager.CATEGORY_DOOR)
