extends Node2D

# Game World - Main game scene that manages the gameplay

@onready var player_manager = $PlayerManager
@onready var camera = $Camera2D
@onready var network_manager = $"/root/NetworkManager"

var local_players = []

# Dungeon generation
var dungeon_data: Dictionary = {}
var dungeon_tilemap_layer: TileMapLayer = null
var dungeon_tilemap_layer_above: TileMapLayer = null
var current_level: int = 1
var dungeon_seed: int = 0

# Level stats tracking
var level_enemies_defeated: int = 0
var level_times_downed: int = 0
var level_exp_collected: float = 0.0
var level_coins_collected: int = 0

# Client ready tracking (server only)
var clients_ready: Dictionary = {} # peer_id -> bool

# Level complete tracking
var level_complete_triggered: bool = false  # Prevent multiple level complete triggers

func _ready():
	# Add to group for easy access
	add_to_group("game_world")
	
	# Connect network signals
	if network_manager:
		network_manager.player_connected.connect(_on_player_connected)
		network_manager.player_disconnected.connect(_on_player_disconnected)
	
	# Generate dungeon on host
	if multiplayer.is_server() or not multiplayer.has_multiplayer_peer():
		print("GameWorld: _ready() - Will generate dungeon (is_server: ", multiplayer.is_server(), ", has_peer: ", multiplayer.has_multiplayer_peer(), ")")
		call_deferred("_generate_dungeon")
	else:
		print("GameWorld: _ready() - Client, will wait for dungeon sync")
		# Clients spawn players immediately (they'll be moved when dungeon syncs)
		call_deferred("_spawn_all_players")

func _spawn_all_players():
	print("GameWorld: Spawning all players. Server: ", multiplayer.is_server())
	print("GameWorld: Players info: ", network_manager.players_info)
	
	# Only spawn on server initially - clients will spawn via RPC
	if multiplayer.is_server():
		for peer_id in network_manager.players_info.keys():
			var info = network_manager.players_info[peer_id]
			print("GameWorld: Server spawning ", info.local_player_count, " players for peer ", peer_id)
			player_manager.spawn_players_for_peer(peer_id, info.local_player_count)

func _on_player_connected(peer_id: int, player_info: Dictionary):
	print("GameWorld: Player connected signal received for peer ", peer_id, " with info: ", player_info)
	
	# Reset ready status for this peer (they need to notify again after spawning)
	if multiplayer.is_server():
		clients_ready[peer_id] = false
		# Reset all_clients_ready flag for all server players when a new client connects
		_reset_server_players_ready_flag()
	
	if multiplayer.is_server():
		var host_room = null
		# Sync existing dungeon to the new client (if dungeon has been generated)
		if not dungeon_data.is_empty():
			print("GameWorld: Syncing existing dungeon to client ", peer_id)
			host_room = _get_host_room()
			_sync_dungeon.rpc_id(peer_id, dungeon_data, dungeon_seed, current_level, host_room)
		
		# Update spawn points to use host's current room
		host_room = _get_host_room()
		if not host_room.is_empty():
			print("GameWorld: Host is in room at ", host_room.x, ", ", host_room.y)
			_update_spawn_points(host_room)
		else:
			print("GameWorld: Could not find host room, using start room")
			_update_spawn_points() # Use start room as fallback
		
		# Server spawns locally
		print("GameWorld: Server spawning players for peer ", peer_id)
		player_manager.spawn_players_for_peer(peer_id, player_info.local_player_count)
		# Sync spawn to all clients
		_sync_spawn_player.rpc(peer_id, player_info.local_player_count)
		
		# Sync existing enemies (from spawners) to the new client
		# Wait a bit after dungeon sync to ensure spawners are spawned first
		call_deferred("_sync_existing_enemies_to_client", peer_id)
		
		# Sync existing chest open states to the new client
		# Wait a bit after dungeon sync to ensure objects are spawned first
		call_deferred("_sync_existing_chest_states_to_client", peer_id)
		
		# Note: Dungeon-spawned enemies are already synced via _sync_dungeon RPC
		# which includes dungeon_data.enemies and calls _spawn_enemies() on the client.
		# So we don't need to sync them again with individual RPCs.
		
		# Note: Interactable objects are also synced via _sync_dungeon RPC
		# which includes dungeon_data.interactable_objects and calls _spawn_interactable_objects() on the client.
		# However, chest open states need to be synced separately since they change during gameplay.
		
		# Sync existing torches to the new client
		call_deferred("_sync_existing_torches_to_client", peer_id)
	else:
		# Clients spawn directly when they receive this signal
		print("GameWorld: Client spawning players for peer ", peer_id)
		player_manager.spawn_players_for_peer(peer_id, player_info.local_player_count)

func _sync_existing_enemies_to_client(client_peer_id: int):
	# Find all enemy spawners and sync their spawned enemies to the new client
	# Spawners are children of the Entities node, not GameWorld directly
	var spawners = []
	var entities_node = get_node_or_null("Entities")
	if entities_node:
		# Find spawners in Entities node
		for child in entities_node.get_children():
			if child.is_in_group("enemy_spawner") or (child.has_method("get_spawned_enemy_positions") and child.has_method("spawn_enemy_at_position")):
				spawners.append(child)
	else:
		# Fallback: search all children (shouldn't happen, but just in case)
		for child in get_children():
			if child.is_in_group("enemy_spawner") or (child.has_method("get_spawned_enemy_positions") and child.has_method("spawn_enemy_at_position")):
				spawners.append(child)
	
	print("GameWorld: Syncing existing enemies to client ", client_peer_id, " from ", spawners.size(), " spawners")
	
	for spawner in spawners:
		var enemy_data = spawner.get_spawned_enemy_positions()
		for data in enemy_data:
			# Use the stored scene_index and humanoid_type for each enemy
			var pos = data.position
			var scene_index = data.scene_index if "scene_index" in data else -1
			var humanoid_type = data.humanoid_type if "humanoid_type" in data else -1
			_sync_enemy_spawn.rpc_id(client_peer_id, spawner.name, pos, scene_index, humanoid_type)
			print("GameWorld: Sent enemy spawn sync to client ", client_peer_id, ": spawner=", spawner.name, " pos=", pos, " scene_index=", scene_index, " humanoid_type=", humanoid_type)

func _on_player_disconnected(peer_id: int):
	print("GameWorld: Player disconnected - ", peer_id)
	player_manager.despawn_players_for_peer(peer_id)

@rpc("authority", "reliable")
func _sync_spawn_player(peer_id: int, local_count: int):
	# Only clients process this RPC (server already spawned)
	if not multiplayer.is_server():
		print("GameWorld: Client received RPC to spawn peer ", peer_id)
		player_manager.spawn_players_for_peer(peer_id, local_count)
		# Client will notify server when ready via _notify_client_ready

@rpc("any_peer", "reliable")
func _notify_client_ready(peer_id: int):
	# Client notifies server that it's ready (all players spawned)
	if multiplayer.is_server():
		print("GameWorld: Client ", peer_id, " is ready")
		clients_ready[peer_id] = true
		# Store the time when this client became ready
		var current_time = Time.get_ticks_msec() / 1000.0
		clients_ready[str(peer_id) + "_ready_time"] = current_time
		# Notify all players that a client is ready (so server players can check if all are ready)
		_client_ready_status_changed.rpc(clients_ready.duplicate())
		# Note: We don't reset the flag here - we want server players to check if all are ready now

@rpc("authority", "reliable")
func _client_ready_status_changed(_ready_status: Dictionary):
	# Server broadcasts ready status to all clients
	# This allows server players to know when all clients are ready
	# Currently not used on clients, but kept for future use
	pass

func _reset_server_players_ready_flag():
	# Reset all_clients_ready flag for all server players
	# This happens when a new client connects, so server players re-check readiness
	# Called directly on server (not via RPC)
	var entities_node = get_node_or_null("Entities")
	if entities_node:
		for child in entities_node.get_children():
			if child.is_in_group("player"):
				# Only reset for server-controlled players (authority = server peer ID)
				if child.get_multiplayer_authority() == multiplayer.get_unique_id():
					child.all_clients_ready = false
					print("GameWorld: Reset all_clients_ready for server player ", child.name)

@rpc("authority", "reliable")
func _sync_smoke_puffs(_spawner_name: String, puff_positions: Array):
	# Clients spawn smoke puffs when server tells them to
	if not multiplayer.is_server():
		var smoke_puff_scene = preload("res://scenes/smoke_puff.tscn")
		if not smoke_puff_scene:
			return
		
		var entities_node = get_node_or_null("Entities")
		if not entities_node:
			return
		
		for pos in puff_positions:
			if pos is Vector2:
				var puff = smoke_puff_scene.instantiate()
				if puff:
					puff.global_position = pos
					puff.z_index = 10
					if puff.has_node("Sprite2D"):
						puff.get_node("Sprite2D").z_index = 10
					entities_node.add_child(puff)

@rpc("authority", "reliable")
func _sync_enemy_spawn(spawner_name: String, spawn_position: Vector2, scene_index: int = -1, humanoid_type: int = -1):
	# Clients spawn enemy when server tells them to
	if not multiplayer.is_server():
		print("GameWorld: Client received RPC to spawn enemy at spawner: ", spawner_name, " position: ", spawn_position, " scene_index: ", scene_index, " humanoid_type: ", humanoid_type)
		
		# Find the spawner node by name (spawners are children of Entities node, not GameWorld)
		var spawner = null
		var entities_node = get_node_or_null("Entities")
		if entities_node:
			spawner = entities_node.get_node_or_null(spawner_name)
		
		# Fallback: try as direct child of GameWorld (for backwards compatibility)
		if not spawner:
			spawner = get_node_or_null(spawner_name)
		
		if not spawner:
			push_error("ERROR: Could not find spawner with name: ", spawner_name, " in Entities node or GameWorld")
			return
		
		if not spawner.has_method("spawn_enemy_at_position"):
			push_error("ERROR: Spawner does not have spawn_enemy_at_position method!")
			return
		
		# Call spawn method on the spawner with scene index and humanoid_type
		spawner.spawn_enemy_at_position(spawn_position, scene_index, humanoid_type)

# Loot ID counter (server only)
var loot_id_counter: int = 0

@rpc("authority", "reliable")
func _sync_loot_spawn(spawn_position: Vector2, loot_type: int, initial_velocity: Vector2, initial_velocity_z: float, loot_id: int = -1):
	# Clients spawn loot when server tells them to
	if not multiplayer.is_server():
		var loot_scene = preload("res://scenes/loot.tscn")
		if not loot_scene:
			return
		
		var loot = loot_scene.instantiate()
		var entities_node = get_node_or_null("Entities")
		if entities_node:
			# Set multiplayer authority to server (peer 1) so RPCs work
			if multiplayer.has_multiplayer_peer():
				loot.set_multiplayer_authority(1)
			# Store unique loot ID for identification
			if loot_id >= 0:
				loot.set_meta("loot_id", loot_id)
			entities_node.add_child(loot)
			loot.global_position = spawn_position
			loot.loot_type = loot_type
			# Set initial velocity before _ready() processes
			loot.velocity = initial_velocity
			loot.velocity_z = initial_velocity_z
			loot.velocity_set_by_spawner = true
			loot.is_airborne = true
			print("Client spawned loot: ", loot_type, " at ", spawn_position, " authority: ", loot.get_multiplayer_authority())

@rpc("authority", "unreliable")
func _sync_enemy_position(enemy_name: String, enemy_index: int, pos: Vector2, vel: Vector2, z_pos: float, dir: int, frame: int, anim: String, frame_num: int, state_value: int):
	# Clients receive enemy position updates from server
	# Find the enemy by name or index
	if multiplayer.is_server():
		return # Server ignores this (it's the sender)
	
	var entities_node = get_node_or_null("Entities")
	if not entities_node:
		return
	
	# Try to find enemy by name first, then by index
	var enemy = null
	for child in entities_node.get_children():
		if child.is_in_group("enemy"):
			if child.name == enemy_name:
				enemy = child
				break
			elif child.has_meta("enemy_index") and child.get_meta("enemy_index") == enemy_index:
				enemy = child
				break
	
	if enemy and enemy.has_method("_sync_position"):
		# Call the enemy's _sync_position method directly (not via RPC)
		enemy._sync_position(pos, vel, z_pos, dir, frame, anim, frame_num, state_value)

@rpc("authority", "reliable")
func _sync_enemy_death(enemy_name: String, enemy_index: int):
	# Clients receive enemy death sync from server
	# Find the enemy by name or index
	if multiplayer.is_server():
		return # Server ignores this (it's the sender)
	
	var entities_node = get_node_or_null("Entities")
	if not entities_node:
		return
	
	# Try to find enemy by name first, then by index
	var enemy = null
	for child in entities_node.get_children():
		if child.is_in_group("enemy"):
			if child.name == enemy_name:
				enemy = child
				break
			elif child.has_meta("enemy_index") and child.get_meta("enemy_index") == enemy_index:
				enemy = child
				break
	
	if enemy and enemy.has_method("_sync_death"):
		# Call the enemy's _sync_death method directly (not via RPC)
		enemy._sync_death()
	else:
		# Enemy not found - might already be freed or never spawned
		# This is okay, just log it
		print("GameWorld: Could not find enemy for death sync: name=", enemy_name, " index=", enemy_index)

@rpc("authority", "reliable")
func _sync_enemy_damage_visual(enemy_name: String, enemy_index: int):
	# Clients receive enemy damage visual sync from server
	# Find the enemy by name or index
	if multiplayer.is_server():
		return # Server ignores this (it's the sender)
	
	var entities_node = get_node_or_null("Entities")
	if not entities_node:
		return
	
	# Try to find enemy by name first, then by index
	var enemy = null
	for child in entities_node.get_children():
		if child.is_in_group("enemy"):
			if child.name == enemy_name:
				enemy = child
				break
			elif child.has_meta("enemy_index") and child.get_meta("enemy_index") == enemy_index:
				enemy = child
				break
	
	if enemy and enemy.has_method("_sync_damage_visual"):
		# Call the enemy's _sync_damage_visual method directly (not via RPC)
		enemy._sync_damage_visual()
	else:
		# Enemy not found - might already be freed or never spawned
		# This is okay, just log it
		print("GameWorld: Could not find enemy for damage visual sync: name=", enemy_name, " index=", enemy_index)

@rpc("any_peer", "reliable")
func _request_loot_pickup(loot_id: int, loot_position: Vector2, player_peer_id: int):
	# Server receives loot pickup request from client
	# Route to the correct loot item
	if not multiplayer.is_server():
		return
	
	var entities_node = get_node_or_null("Entities")
	if not entities_node:
		return
	
	# Find loot by ID or position
	var loot = null
	for child in entities_node.get_children():
		if child.is_in_group("loot") or child.has_method("_request_pickup"):
			# Check by ID first
			if child.has_meta("loot_id") and child.get_meta("loot_id") == loot_id:
				loot = child
				break
			# Fallback: check by position (within 16 pixels tolerance)
			elif child.global_position.distance_to(loot_position) < 16.0:
				loot = child
				break
	
	if loot and loot.has_method("_request_pickup"):
		# Call the loot's _request_pickup method directly (it will handle the rest)
		loot._request_pickup(player_peer_id)
	else:
		print("GameWorld: Could not find loot for pickup request: id=", loot_id, " pos=", loot_position)

@rpc("authority", "reliable")
func _sync_player_exit_stairs(player_peer_id: int):
	# Client receives notification that a player reached exit stairs
	if multiplayer.is_server():
		return  # Server ignores this (it's the sender)
	
	# Find the player by peer ID
	var players = get_tree().get_nodes_in_group("player")
	var target_player = null
	for p in players:
		if p.has_method("get_multiplayer_authority") and p.get_multiplayer_authority() == player_peer_id:
			target_player = p
			break
	
	# Only disable controls/collision if this is our local player
	if target_player:
		var my_peer_id = multiplayer.get_unique_id()
		if player_peer_id == my_peer_id:
			# This is our local player - disable controls and collision
			target_player.controls_disabled = true
			target_player.set_collision_layer_value(1, false)
			print("GameWorld: Client disabled controls and collision for local player ", target_player.name)
			# Show black fade overlay for this player
			_show_black_fade_overlay()

@rpc("authority", "reliable")
func _sync_show_level_complete(level_time: float):
	# Clients receive level complete UI sync from server
	if multiplayer.is_server():
		return # Server ignores this (it's the sender)
	
	# Stop HUD timer when level completes (on clients too)
	var hud = get_node_or_null("IngameHUD")
	if hud and hud.has_method("stop_timer"):
		hud.stop_timer()
	
	# Show level complete UI (each client will show their own local player's stats)
	_show_level_complete_ui(level_time)

@rpc("authority", "reliable")
func _sync_hide_level_complete():
	# Clients receive hide level complete UI sync from server
	if multiplayer.is_server():
		return # Server ignores this (it's the sender)
	
	var level_complete_ui = get_node_or_null("LevelCompleteUI")
	if level_complete_ui:
		level_complete_ui.visible = false

@rpc("authority", "reliable")
func _sync_restore_player_controls():
	# Clients receive restore controls/collision sync from server
	if multiplayer.is_server():
		return  # Server ignores this (it's the sender)
	
	# Restore controls and collision for local player
	var my_peer_id = multiplayer.get_unique_id()
	var players = get_tree().get_nodes_in_group("player")
	for player in players:
		if player.has_method("get_multiplayer_authority") and player.get_multiplayer_authority() == my_peer_id:
			player.controls_disabled = false
			player.set_collision_layer_value(1, true)
			print("GameWorld: Client restored controls and collision for local player ", player.name)
			break

@rpc("authority", "reliable")
func _sync_remove_black_fade():
	# Clients receive remove black fade sync from server
	if multiplayer.is_server():
		return  # Server ignores this (it's the sender)
	
	_remove_black_fade_overlay()

@rpc("authority", "reliable")
func _sync_show_level_number(level: int):
	# Clients receive level number UI sync from server
	if multiplayer.is_server():
		return # Server ignores this (it's the sender)
	
	current_level = level
	_show_level_number()

@rpc("authority", "reliable")
func _sync_loot_remove(loot_id: int, loot_position: Vector2):
	# Clients receive loot removal sync from server
	# Find the loot by ID or position
	if multiplayer.is_server():
		return # Server ignores this (it's the sender)
	
	var entities_node = get_node_or_null("Entities")
	if not entities_node:
		return
	
	# Try to find loot by ID first, then by position
	var loot = null
	for child in entities_node.get_children():
		if child.is_in_group("loot") or child.has_method("_sync_remove"):
			# Check by ID first
			if child.has_meta("loot_id") and child.get_meta("loot_id") == loot_id:
				loot = child
				break
			# Fallback: check by position (within 16 pixels tolerance)
			elif child.global_position.distance_to(loot_position) < 16.0:
				loot = child
				break
	
	if loot and loot.has_method("_sync_remove"):
		# Call the loot's _sync_remove method directly (not via RPC)
		loot._sync_remove()
	else:
		# Loot not found - might already be freed or never spawned
		# This is okay, just log it
		print("GameWorld: Could not find loot for removal sync: id=", loot_id, " pos=", loot_position)

func _process(_delta):
	# Update camera to follow local players
	_update_camera()

func _update_camera():
	local_players = player_manager.get_local_players()
	
	if local_players.is_empty():
		return
	
	# Calculate center of all local players
	var center = Vector2.ZERO
	for player in local_players:
		center += player.position
	center /= local_players.size()
	
	# Smooth camera movement
	camera.position = camera.position.lerp(center, 0.1)
	
	# Adjust zoom based on player spread (for split-screen effect)
	if local_players.size() > 1:
		var max_distance = 0.0
		for player in local_players:
			var distance = center.distance_to(player.position)
			max_distance = max(max_distance, distance)
		
		# Adjust zoom to fit all players
		var target_zoom = clamp(800.0 / (max_distance + 400.0), 0.5, 1.5)
		camera.zoom = camera.zoom.lerp(Vector2.ONE * target_zoom, 0.05)

func _generate_dungeon():
	print("GameWorld: _generate_dungeon() called - is_server: ", multiplayer.is_server(), ", has_peer: ", multiplayer.has_multiplayer_peer())
	
	if not multiplayer.is_server() and multiplayer.has_multiplayer_peer():
		print("GameWorld: Not server, skipping dungeon generation")
		return
	
	# Reset level complete flag for new level
	level_complete_triggered = false
	
	print("GameWorld: Generating dungeon level ", current_level)
	
	# Generate seed (deterministic for level 1, can be random for future levels)
	if dungeon_seed == 0:
		dungeon_seed = randi() if current_level > 1 else 12345 # Fixed seed for level 1 for testing
	
	# Create dungeon generator
	var generator = load("res://scripts/dungeon_generator.gd").new()
	var map_size = Vector2i(72, 72) # 72x72 tiles
	
	# Generate dungeon (pass current level for scaling)
	dungeon_data = generator.generate_dungeon(map_size, dungeon_seed, current_level)
	
	if dungeon_data.is_empty():
		push_error("ERROR: Dungeon generation returned empty data!")
		return
	
	print("GameWorld: Dungeon generated with ", dungeon_data.rooms.size(), " rooms")
	print("GameWorld: Start room at ", dungeon_data.start_room.x, ", ", dungeon_data.start_room.y)
	print("GameWorld: Map size: ", dungeon_data.map_size)
	
	# Render dungeon
	_render_dungeon()
	
	# Spawn torches
	_spawn_torches()
	
	# Spawn enemies
	_spawn_enemies()
	
	# Spawn interactable objects
	_spawn_interactable_objects()
	
	# Spawn blocking doors
	_spawn_blocking_doors()
	
	# Spawn room triggers
	_spawn_room_triggers()
	
	# Wait a frame to ensure enemies and objects are properly in scene tree before syncing
	await get_tree().process_frame
	
	# Update player spawn points based on start room
	_update_spawn_points()
	
	# Spawn players for all connected peers (after dungeon is generated and spawn points are set)
	# This ensures players spawn at the correct location
	_spawn_all_players()
	
	# Move any already-spawned players to the correct spawn points
	_move_all_players_to_start_room()
	
	# Update camera immediately to ensure it's looking at the players
	await get_tree().process_frame # Wait a frame for players to be fully in scene tree
	_update_camera()
	
	# Show level number (for initial level generation only - not when called from level completion)
	# Check if this is initial generation by checking if we're in _ready or if level is 1
	# For level completion, the level number is shown after _generate_dungeon() completes
	if current_level == 1:
		_show_level_number()
		# Sync to all clients
		if multiplayer.has_multiplayer_peer():
			_sync_show_level_number.rpc(current_level)
	
	# Load HUD after dungeon generation completes (non-blocking)
	call_deferred("_load_hud")
	
	# Sync dungeon to all clients
	if multiplayer.has_multiplayer_peer():
		# Get host's current room for spawning new players near host
		var host_room = _get_host_room()
		
		# Debug: Check if enemies are in dungeon_data before syncing
		if dungeon_data.has("enemies"):
			print("GameWorld: Server syncing dungeon with ", dungeon_data.enemies.size(), " enemies")
		else:
			print("GameWorld: WARNING: Server dungeon_data has NO 'enemies' key before sync!")
		
		_sync_dungeon.rpc(dungeon_data, dungeon_seed, current_level, host_room)
	
	print("GameWorld: Dungeon generation completed successfully")

func _render_dungeon():
	if dungeon_data.is_empty():
		push_error("ERROR: Cannot render dungeon - no dungeon data!")
		return
	
	# Try to use existing TileMapLayer from scene, or create new one
	var environment = get_node_or_null("Environment")
	
	if environment:
		dungeon_tilemap_layer = environment.get_node_or_null("DungeonLayer0")
		dungeon_tilemap_layer_above = environment.get_node_or_null("TileMapLayerAbove")
	
	if not dungeon_tilemap_layer:
		# Create new TileMapLayer
		print("GameWorld: Creating new TileMapLayer")
		dungeon_tilemap_layer = TileMapLayer.new()
		dungeon_tilemap_layer.name = "DungeonLayer0"
		
		# Add to scene
		if environment:
			environment.add_child(dungeon_tilemap_layer)
		else:
			add_child(dungeon_tilemap_layer)
			move_child(dungeon_tilemap_layer, 0)
		
		dungeon_tilemap_layer.position = Vector2.ZERO
		print("GameWorld: Created new TileMapLayer and added to scene")
	else:
		print("GameWorld: Using existing TileMapLayer from scene")
	
	if not dungeon_tilemap_layer_above:
		# Create new TileMapLayerAbove
		print("GameWorld: Creating new TileMapLayerAbove")
		dungeon_tilemap_layer_above = TileMapLayer.new()
		dungeon_tilemap_layer_above.name = "TileMapLayerAbove"
		
		# Add to scene
		if environment:
			environment.add_child(dungeon_tilemap_layer_above)
		else:
			add_child(dungeon_tilemap_layer_above)
			move_child(dungeon_tilemap_layer_above, 0)
		
		dungeon_tilemap_layer_above.position = Vector2.ZERO
		print("GameWorld: Created new TileMapLayerAbove and added to scene")
	else:
		print("GameWorld: Using existing TileMapLayerAbove from scene")
	
	# TileMapLayer should work standalone - no TileMap needed
	print("GameWorld: TileMapLayer ready for rendering")
	
	# Render tiles from dungeon_data
	var tile_grid = dungeon_data.tile_grid
	var grid = dungeon_data.grid
	var map_size = dungeon_data.map_size
	
	print("GameWorld: Rendering ", map_size.x, "x", map_size.y, " tiles")
	var tiles_placed = 0
	var above_tiles_placed = 0
	
	const BLACK_TILE = Vector2i(2, 2) # Black tile for non-floor/wall/door
	
	for x in range(map_size.x):
		for y in range(map_size.y):
			var tile_coords = tile_grid[x][y]
			var grid_value = grid[x][y]
			
			# Render main layer - set a tile for EVERY position
			var main_tile: Vector2i
			
			# Determine what tile to use based on grid value
			# Only use the tile_coords if it's a valid tile (not 0,0) or if it's a wall
			if grid_value == 0:
				# Wall - use the tile_coords (which should be a wall tile, can be 0,0 for top-left corner)
				main_tile = tile_coords
			elif grid_value == 1:
				# Floor - use the tile_coords (which should be a floor tile)
				main_tile = tile_coords
			elif grid_value == 2:
				# Door - use the tile_coords (which should be a door tile)
				main_tile = tile_coords
			elif grid_value == 3:
				# Corridor - use the tile_coords (which should be a floor tile)
				main_tile = tile_coords
			elif grid_value == 4:
				# Stairs - use the tile_coords (which should be a stairs tile)
				main_tile = tile_coords
			else:
				# Anything else (empty/void) - use black tile (2,2)
				main_tile = BLACK_TILE
			
			# If tile_coords is (0,0) and it's not a wall, use black tile instead
			if tile_coords == Vector2i(0, 0) and grid_value != 0:
				main_tile = BLACK_TILE
			
			# Always explicitly set a tile for every position (prevents default 0,0)
			dungeon_tilemap_layer.set_cell(Vector2i(x, y), 0, main_tile)
			if main_tile != Vector2i(0, 0): # Count non-default tiles
				tiles_placed += 1
			
			# Render above layer
			# Render tile (2,2) for every tile that is NOT floor, room-walls, or doors
			# This includes corridors and any other tiles
			if grid_value == 0:
				# Wall - check if it's actually a wall tile or just empty space
				# Empty spaces have tile_coords == (0,0), actual walls have other tile_coords
				if tile_coords == Vector2i(0, 0):
					# Empty space - render black tile (2,2)
					dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, BLACK_TILE)
					above_tiles_placed += 1
				# else: actual wall - don't render anything in above layer
			elif grid_value == 1:
				# Floor - don't render anything in above layer
				pass
			elif grid_value == 2:
				# Door - render specific door parts
				var current_tile = tile_coords
				
				# Check which door type this is based on tile coordinates
				# Door UP: first row (y=0) - tiles (7,0), (8,0), (9,0)
				if current_tile.y == 0 and current_tile.x >= 7 and current_tile.x <= 9:
					dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
					above_tiles_placed += 1
				
				# Door RIGHT: second column (x=11) - tiles (11,2), (11,3), (11,4)
				elif current_tile.x == 11 and current_tile.y >= 2 and current_tile.y <= 4:
					dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
					above_tiles_placed += 1
				
				# Door LEFT: first column (x=5) - tiles (5,2), (5,3), (5,4)
				elif current_tile.x == 5 and current_tile.y >= 2 and current_tile.y <= 4:
					dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
					above_tiles_placed += 1
				
				# Door DOWN: second row (y=6) - tiles (7,6), (8,6), (9,6)
				elif current_tile.y == 6 and current_tile.x >= 7 and current_tile.x <= 9:
					dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
					above_tiles_placed += 1
			elif grid_value == 4: # Stairs - render similar to doors
				var current_tile = tile_coords
				# Render stairs parts similar to doors
				# Stairs use the same structure as doors but with special middle frame tiles
				# Stairs UP: first row (y=0) - tiles (7,0), (10,0), (9,0) - middle tile is (10,0) instead of (8,0)
				if current_tile.y == 0 and current_tile.x >= 7 and current_tile.x <= 9:
					dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
					above_tiles_placed += 1
				# Special case: UP stairs middle tile (10,0)
				elif current_tile == Vector2i(10, 0):
					dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
					above_tiles_placed += 1
				# Stairs RIGHT: columns (x=10 or x=11) - tiles (10,2), (11,1), (10,3), (11,3), (10,4), (11,4)
				# Middle tile is (11,1) instead of (11,3)
				elif (current_tile.x == 10 or current_tile.x == 11) and current_tile.y >= 2 and current_tile.y <= 4:
					dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
					above_tiles_placed += 1
				# Special case: RIGHT stairs middle tile (11,1)
				elif current_tile == Vector2i(11, 1):
					dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
					above_tiles_placed += 1
				# Stairs LEFT: first column (x=5) - tiles (5,2), (5,1), (5,3), (5,4)
				# Middle tile is (5,1) instead of (5,3)
				elif current_tile.x == 5 and current_tile.y >= 1 and current_tile.y <= 4:
					dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
					above_tiles_placed += 1
				# Stairs DOWN: second row (y=5) - tiles (7,5), (6,6), (9,5)
				# Middle tile is (6,6) instead of (8,6)
				elif current_tile.y == 5 and current_tile.x >= 7 and current_tile.x <= 9:
					dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
					above_tiles_placed += 1
				# Special case: DOWN stairs middle tile (6,6)
				elif current_tile == Vector2i(6, 6):
					dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, current_tile)
					above_tiles_placed += 1
			else:
				# Everything else (corridors, empty tiles, etc.) - render black tile (2,2)
				dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, BLACK_TILE)
				above_tiles_placed += 1
	
	print("GameWorld: Placed ", tiles_placed, " tiles on main layer")
	print("GameWorld: Placed ", above_tiles_placed, " tiles on above layer")
	print("GameWorld: Dungeon rendered on TileMapLayer")
	
	# Create stairs Area2D if stairs data exists
	_create_stairs_area()

func _update_spawn_points(target_room: Dictionary = {}, clear_existing: bool = true):
	# Update player manager spawn points based on a room
	# If target_room is empty, use start room (for initial spawn)
	# Otherwise use the provided room (for spawning new players near host)
	# clear_existing: If true, clear existing spawn points first (for respawn/new room)
	if not dungeon_data.has("start_room"):
		return
	
	var room = target_room if not target_room.is_empty() else dungeon_data.start_room
	var tile_size = 16 # 16 pixels per tile
	
	# Get already assigned spawn positions to exclude them (if not clearing)
	var exclude_positions = []
	if not clear_existing:
		for spawn_point in player_manager.spawn_points:
			exclude_positions.append(spawn_point)
	
	# Clear existing spawn points if requested
	if clear_existing:
		player_manager.spawn_points.clear()
	
	# Find free floor tiles in the room (excluding already assigned positions)
	var free_tiles = _find_free_floor_tiles_in_room(room, exclude_positions)
	
	# Update player manager spawn points
	if free_tiles.size() > 0:
		# Use free floor tiles as spawn points
		for tile_pos in free_tiles:
			var world_x = tile_pos.x * tile_size + tile_size / 2.0 # Center of tile
			var world_y = tile_pos.y * tile_size + tile_size / 2.0 # Center of tile
			player_manager.spawn_points.append(Vector2(world_x, world_y))
		print("GameWorld: Updated spawn points with ", free_tiles.size(), " free floor tiles in room")
	else:
		# Fallback: Create spawn points in a circle around the room center
		var room_center_x = (room.x + room.w / 2.0) * tile_size
		var room_center_y = (room.y + room.h / 2.0) * tile_size
		var num_spawn_points = 8
		for i in range(num_spawn_points):
			var angle = i * PI * 2 / num_spawn_points
			var offset = Vector2(cos(angle), sin(angle)) * 30 # 30 pixel radius
			player_manager.spawn_points.append(Vector2(room_center_x, room_center_y) + offset)
		print("GameWorld: Updated spawn points in circle around room center (no free tiles found)")

func _find_room_at_position(world_pos: Vector2) -> Dictionary:
	# Find which room contains the given world position
	if dungeon_data.is_empty() or not dungeon_data.has("rooms"):
		return {}
	
	var tile_size = 16 # 16 pixels per tile
	var tile_x = int(world_pos.x / tile_size)
	var tile_y = int(world_pos.y / tile_size)
	
	# Check each room to see if the position is inside it
	for room in dungeon_data.rooms:
		# Room interior is from room.x + 2 to room.x + room.w - 2 (excluding 2-tile walls)
		if tile_x >= room.x + 2 and tile_x < room.x + room.w - 2 and \
		   tile_y >= room.y + 2 and tile_y < room.y + room.h - 2:
			return room
	
	return {}

func _find_free_floor_tiles_in_room(room: Dictionary, exclude_positions: Array = []) -> Array:
	# Find all free floor tiles in a room (tiles that are floor and not occupied)
	# exclude_positions: Array of Vector2 world positions to exclude (for players already assigned spawn points)
	if dungeon_data.is_empty() or not dungeon_data.has("grid"):
		return []
	
	var free_tiles = []
	var grid = dungeon_data.grid
	var map_size = dungeon_data.map_size
	
	# Room interior is from room.x + 2 to room.x + room.w - 2 (excluding 2-tile walls)
	for x in range(room.x + 2, room.x + room.w - 2):
		for y in range(room.y + 2, room.y + room.h - 2):
			if x >= 0 and x < map_size.x and y >= 0 and y < map_size.y:
				# Check if it's a floor tile
				if grid[x][y] == 1: # Floor
					var tile_world_x = x * 16 + 8 # Center of tile
					var tile_world_y = y * 16 + 8 # Center of tile
					var tile_world_pos = Vector2(tile_world_x, tile_world_y)
					
					# Check if this position is in the exclude list
					var is_excluded = false
					for excluded_pos in exclude_positions:
						if tile_world_pos.distance_to(excluded_pos) < 32:
							is_excluded = true
							break
					
					if is_excluded:
						continue
					
					# Check if there's already a player at this position
					var is_free = true
					for player in player_manager.get_all_players():
						if tile_world_pos.distance_to(player.position) < 32:
							is_free = false
							break
					
					if is_free:
						free_tiles.append(Vector2i(x, y))
	
	# Shuffle free tiles to randomize spawn positions
	free_tiles.shuffle()
	
	return free_tiles

func _is_safe_spawn_position(world_pos: Vector2) -> bool:
	# Check if a world position is safe for spawning (on a floor tile)
	if dungeon_data.is_empty() or not dungeon_data.has("grid"):
		return false
	
	var tile_size = 16
	var tile_x = int(world_pos.x / tile_size)
	var tile_y = int(world_pos.y / tile_size)
	var grid = dungeon_data.grid
	var map_size = dungeon_data.map_size
	
	# Check bounds
	if tile_x < 0 or tile_y < 0 or tile_x >= map_size.x or tile_y >= map_size.y:
		return false
	
	# Check if it's a floor tile
	if grid[tile_x][tile_y] == 1: # Floor
		return true
	
	return false

func _find_nearby_safe_spawn_position(world_pos: Vector2, max_distance: float = 64.0) -> Vector2:
	# Find a nearby safe spawn position (on a floor tile)
	# Returns the original position if it's safe, otherwise finds the nearest safe position
	# max_distance: Maximum distance to search for a safe position
	# First check if the original position is safe
	if _is_safe_spawn_position(world_pos):
		return world_pos
	
	# Search in expanding circles around the position
	var tile_size = 16
	var search_radius = 1 # Start with 1 tile radius
	var max_radius = int(max_distance / tile_size) + 1
	
	while search_radius <= max_radius:
		# Check all tiles in a square around the position
		var center_tile_x = int(world_pos.x / tile_size)
		var center_tile_y = int(world_pos.y / tile_size)
		
		# Check tiles in a square pattern
		for dx in range(-search_radius, search_radius + 1):
			for dy in range(-search_radius, search_radius + 1):
				# Skip if outside the search radius (only check the perimeter)
				if abs(dx) != search_radius and abs(dy) != search_radius:
					continue
				
				var check_tile_x = center_tile_x + dx
				var check_tile_y = center_tile_y + dy
				var check_world_pos = Vector2(check_tile_x * tile_size + tile_size / 2.0, check_tile_y * tile_size + tile_size / 2.0)
				
				# Check if this position is safe
				if _is_safe_spawn_position(check_world_pos):
					print("GameWorld: Found safe spawn position at ", check_world_pos, " (original was ", world_pos, ")")
					return check_world_pos
		
		search_radius += 1
	
	# If no safe position found, return original (fallback)
	print("GameWorld: WARNING: Could not find safe spawn position near ", world_pos, ", using original position")
	return world_pos

func _get_host_room() -> Dictionary:
	# Get the room where the host (server peer) is currently located
	if not multiplayer.is_server():
		return {}
	
	# Find the host's players (server peer is usually peer_id 1)
	var host_peer_id = 1
	var host_players = []
	for player in player_manager.get_all_players():
		if player.peer_id == host_peer_id:
			host_players.append(player)
	
	# If no host players found, try to find any server player
	if host_players.is_empty():
		host_players = player_manager.get_all_players()
	
	if host_players.is_empty():
		return {}
	
	# Use the first host player's position to find the room
	var host_pos = host_players[0].position
	return _find_room_at_position(host_pos)

@rpc("authority", "reliable")
func _sync_dungeon(dungeon_data_sync: Dictionary, seed_value: int, level: int, host_room: Dictionary = {}):
	# Clients receive dungeon data from host
	if not multiplayer.is_server():
		print("GameWorld: Client received dungeon sync for level ", level)
		print("GameWorld: dungeon_data_sync keys: ", dungeon_data_sync.keys())
		if dungeon_data_sync.has("enemies"):
			var enemy_count = dungeon_data_sync.enemies.size() if dungeon_data_sync.enemies is Array else 0
			print("GameWorld: dungeon_data_sync has ", enemy_count, " enemies")
		else:
			print("GameWorld: WARNING: dungeon_data_sync has NO 'enemies' key!")
		
		dungeon_data = dungeon_data_sync
		dungeon_seed = seed_value
		current_level = level # Update current_level FIRST before showing level number
		print("GameWorld: Client updated current_level to ", current_level, " from sync")
		
		# Clear previous level on client
		_clear_level()
		
		# Wait for old entities to be fully freed before spawning new ones
		await get_tree().process_frame
		await get_tree().process_frame # Wait an extra frame to ensure cleanup is complete
		
		# Render dungeon on client
		_render_dungeon()
		
		# Spawn torches on client
		_spawn_torches()
		
		# Spawn enemies on client
		_spawn_enemies()
		
		# Spawn interactable objects on client
		_spawn_interactable_objects()
		
		# Spawn blocking doors on client
		_spawn_blocking_doors()
		
		# Spawn room triggers on client
		_spawn_room_triggers()
		
		# Wait a frame to ensure all enemies and objects are properly added to scene tree and initialized
		await get_tree().process_frame
		await get_tree().process_frame # Wait an extra frame to ensure enemies are fully ready
		
		# Update spawn points - use host's room if available, otherwise use start room
		if not host_room.is_empty():
			print("GameWorld: Using host's room for spawn points")
			_update_spawn_points(host_room)
			# Move any existing players to spawn near host
			_move_players_to_host_room(host_room)
		else:
			print("GameWorld: Host room not available, using start room")
			_update_spawn_points()
		# Move all players to start room
		_move_all_players_to_start_room()
		
		# Note: Level number is shown via _sync_show_level_number RPC, not here
		# This prevents duplicate displays and ensures consistent timing
		
		# Load HUD on client (same as server does)
		call_deferred("_load_hud")
		
		# Sync existing dungeon to newly connected clients
		if multiplayer.is_server():
			# This shouldn't happen, but just in case
			pass

func _spawn_torches():
	# Spawn torches from dungeon data
	if dungeon_data.is_empty() or not dungeon_data.has("torches"):
		return
	
	var torch_scene = preload("res://scenes/torch_wall.tscn")
	if not torch_scene:
		push_error("ERROR: Could not load torch_wall.tscn!")
		return
	
	var entities_node = get_node_or_null("Entities")
	if not entities_node:
		push_error("ERROR: Could not find Entities node!")
		return
	
	# Remove existing torches first
	for child in entities_node.get_children():
		if child.name.begins_with("Torch_"):
			child.queue_free()
	
	# Spawn torches
	var torches = dungeon_data.torches
	print("GameWorld: Spawning ", torches.size(), " torches")
	
	for i in range(torches.size()):
		var torch_data = torches[i]
		var torch = torch_scene.instantiate()
		torch.name = "Torch_%d" % i
		entities_node.add_child(torch)
		torch.global_position = torch_data.position
		torch.rotation_degrees = torch_data.rotation
	
	print("GameWorld: Spawned ", torches.size(), " torches")

func _spawn_enemies():
	# Spawn enemies from dungeon data
	if dungeon_data.is_empty() or not dungeon_data.has("enemies"):
		return
	
	# On server: spawn enemies with full authority
	# On clients: spawn enemies but they'll be authority of server
	var is_server = multiplayer.is_server() or not multiplayer.has_multiplayer_peer()
	
	var entities_node = get_node_or_null("Entities")
	if not entities_node:
		push_error("ERROR: Could not find Entities node!")
		return
	
	# Remove existing enemies first (except ones spawned by spawners)
	# Also remove dead enemies that might still be in the scene
	# Collect all enemies to remove first, then remove them
	var enemies_to_remove = []
	for child in entities_node.get_children():
		if child.is_in_group("enemy") and child.has_meta("dungeon_spawned"):
			enemies_to_remove.append(child)
	
	# Remove all old dungeon enemies
	for enemy in enemies_to_remove:
		print("GameWorld: Removing old dungeon enemy: ", enemy.name)
		if is_instance_valid(enemy):
			enemy.queue_free()
	
	# Spawn enemies
	if not dungeon_data.has("enemies"):
		print("GameWorld: WARNING: dungeon_data has no 'enemies' key!")
		return
	
	var enemies = dungeon_data.enemies
	if enemies == null or not enemies is Array:
		print("GameWorld: WARNING: dungeon_data.enemies is not an Array! Type: ", typeof(enemies))
		return
	
	print("GameWorld: Spawning ", enemies.size(), " enemies (is_server: ", is_server, ")")
	
	for i in range(enemies.size()):
		var enemy_data = enemies[i]
		if not enemy_data is Dictionary:
			push_error("ERROR: Enemy data at index ", i, " is not a Dictionary! Type: ", typeof(enemy_data))
			continue
		
		if not enemy_data.has("type"):
			push_error("ERROR: Enemy data missing 'type' field: ", enemy_data)
			continue
		
		if not enemy_data.has("position"):
			push_error("ERROR: Enemy data missing 'position' field: ", enemy_data)
			continue
		
		var enemy_type = enemy_data.type
		if not enemy_type is String:
			push_error("ERROR: Enemy type is not a String! Type: ", typeof(enemy_type), " Value: ", enemy_type)
			continue
		
		if not enemy_type.begins_with("res://"):
			# If type is just "enemy_rat", convert to full path
			enemy_type = "res://scenes/" + enemy_type + ".tscn"
		
		var enemy_scene = load(enemy_type)
		if not enemy_scene:
			push_error("ERROR: Could not load enemy scene: ", enemy_type)
			continue
		
		var enemy = enemy_scene.instantiate()
		# Use consistent naming: Enemy_<index> based on position in enemies array
		# This ensures server and client enemies have the same names
		enemy.name = "Enemy_%d" % i
		enemy.set_meta("dungeon_spawned", true)
		# Store enemy index for identification
		enemy.set_meta("enemy_index", i)
		
		# Store room reference for AI (if available)
		if enemy_data.has("room") and enemy_data.room is Dictionary:
			enemy.set_meta("room", enemy_data.room)
		
		# Set multiplayer authority BEFORE adding to scene tree (CRITICAL for RPC to work!)
		if multiplayer.has_multiplayer_peer():
			enemy.set_multiplayer_authority(1)
			print("GameWorld: Set enemy authority to 1 BEFORE add_child: ", enemy.name, " authority: ", enemy.get_multiplayer_authority())
		
		# Set enemy stats BEFORE adding to scene (so _ready() can use them)
		if "max_health" in enemy_data:
			enemy.max_health = enemy_data.max_health
			enemy.current_health = enemy_data.max_health
		if "move_speed" in enemy_data:
			enemy.move_speed = enemy_data.move_speed
		if "damage" in enemy_data:
			enemy.damage = enemy_data.damage
		
		# If it's a humanoid enemy, set the humanoid_type
		if enemy_type.ends_with("enemy_humanoid.tscn") and "humanoid_type" in enemy_data:
			enemy.humanoid_type = enemy_data.humanoid_type
		
		# CRITICAL: Set collision mask BEFORE adding to scene to ensure enemies collide with walls (layer 7 = bit 6 = 64)
		# This overrides any collision_mask set in the scene file
		enemy.collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64)
		
		# Add to scene tree AFTER setting authority and stats
		entities_node.add_child(enemy)
		enemy.global_position = enemy_data.position
		
		# CRITICAL: Re-verify collision_mask AFTER adding to scene (in case scene file overrides it)
		# This ensures enemies always collide with walls (layer 7 = bit 6 = 64)
		if enemy.collision_mask != (1 | 2 | 64):
			print("GameWorld: WARNING - Enemy ", enemy.name, " collision_mask was ", enemy.collision_mask, ", expected ", (1 | 2 | 64), "! Correcting...")
			enemy.collision_mask = 1 | 2 | 64
		
		# Verify authority is still set after adding to tree
		if multiplayer.has_multiplayer_peer():
			var auth_after = enemy.get_multiplayer_authority()
			print("GameWorld: Enemy authority AFTER add_child: ", enemy.name, " authority: ", auth_after, " is_authority: ", enemy.is_multiplayer_authority())
			if auth_after != 1 and is_server:
				push_error("GameWorld: ERROR - Enemy authority lost after add_child! Expected 1, got ", auth_after)
		
		# Ensure enemy is fully initialized
		if not enemy.is_inside_tree():
			push_error("GameWorld: ERROR - Enemy not in tree after add_child!")
		
		if is_server:
			print("GameWorld: Server spawned enemy: ", enemy.name, " at ", enemy_data.position, " (type: ", enemy_type, ") authority: ", enemy.get_multiplayer_authority(), " in_tree: ", enemy.is_inside_tree(), " is_authority: ", enemy.is_multiplayer_authority(), " index: ", i)
		else:
			print("GameWorld: Client spawned enemy: ", enemy.name, " at ", enemy_data.position, " (type: ", enemy_type, ") authority: ", enemy.get_multiplayer_authority(), " is_authority: ", enemy.is_multiplayer_authority(), " in_tree: ", enemy.is_inside_tree(), " index: ", i)
	
	if is_server:
		print("GameWorld: Server spawned ", enemies.size(), " enemies")
	else:
		print("GameWorld: Client spawned ", enemies.size(), " enemies")

func _spawn_interactable_objects():
	# Spawn interactable objects from dungeon data
	if dungeon_data.is_empty() or not dungeon_data.has("interactable_objects"):
		return
	
	# On server: spawn objects with full authority
	# On clients: spawn objects but they'll be authority of server
	var is_server = multiplayer.is_server() or not multiplayer.has_multiplayer_peer()
	
	var entities_node = get_node_or_null("Entities")
	if not entities_node:
		push_error("ERROR: Could not find Entities node!")
		return
	
	# Remove existing interactable objects first
	var objects_to_remove = []
	for child in entities_node.get_children():
		if child.is_in_group("interactable_object") and child.has_meta("dungeon_spawned"):
			objects_to_remove.append(child)
	
	# Remove all old dungeon objects
	for obj in objects_to_remove:
		print("GameWorld: Removing old dungeon interactable object: ", obj.name)
		if is_instance_valid(obj):
			obj.queue_free()
	
	# Spawn objects
	if not dungeon_data.has("interactable_objects"):
		print("GameWorld: WARNING: dungeon_data has no 'interactable_objects' key!")
		return
	
	var objects = dungeon_data.interactable_objects
	if objects == null or not objects is Array:
		print("GameWorld: WARNING: dungeon_data.interactable_objects is not an Array! Type: ", typeof(objects))
		return
	
	print("GameWorld: Spawning ", objects.size(), " interactable objects (is_server: ", is_server, ")")
	
	var interactable_object_scene = load("res://scenes/interactable_object.tscn")
	if not interactable_object_scene:
		push_error("ERROR: Could not load interactable_object scene!")
		return
	
	for i in range(objects.size()):
		var object_data = objects[i]
		if not object_data is Dictionary:
			push_error("ERROR: Object data at index ", i, " is not a Dictionary! Type: ", typeof(object_data))
			continue
		
		if not object_data.has("type"):
			push_error("ERROR: Object data missing 'type' field: ", object_data)
			continue
		
		if not object_data.has("position"):
			push_error("ERROR: Object data missing 'position' field: ", object_data)
			continue
		
		if not object_data.has("setup_function"):
			push_error("ERROR: Object data missing 'setup_function' field: ", object_data)
			continue
		
		var obj = interactable_object_scene.instantiate()
		# Use consistent naming: InteractableObject_<index>
		obj.name = "InteractableObject_%d" % i
		obj.set_meta("dungeon_spawned", true)
		obj.set_meta("object_index", i)
		
		# Store room reference (if available)
		if object_data.has("room") and object_data.room is Dictionary:
			obj.set_meta("room", object_data.room)
		
		# Set multiplayer authority BEFORE adding to scene tree
		if multiplayer.has_multiplayer_peer():
			obj.set_multiplayer_authority(1)
		
		# Add to scene tree
		entities_node.add_child(obj)
		obj.global_position = object_data.position
		
		# Call the setup function to configure the object
		if obj.has_method(object_data.setup_function):
			obj.call(object_data.setup_function)
		else:
			push_error("ERROR: Object does not have method: ", object_data.setup_function)
		
		# Add to group for easy access
		obj.add_to_group("interactable_object")
	
	print("GameWorld: Spawned ", objects.size(), " interactable objects")

func _sync_existing_dungeon_enemies_to_client(client_peer_id: int):
	# Sync existing dungeon-spawned enemies to newly connected client
	# Use dungeon_data.enemies array (like torches) instead of searching scene tree
	if dungeon_data.is_empty() or not dungeon_data.has("enemies"):
		return
	
	var enemies = dungeon_data.enemies
	print("GameWorld: Syncing ", enemies.size(), " dungeon-spawned enemies to client ", client_peer_id)
	
	# Sync each enemy from dungeon data
	for enemy_data in enemies:
		_sync_dungeon_enemy_spawn.rpc_id(client_peer_id, enemy_data)

@rpc("authority", "reliable")
func _sync_dungeon_enemy_spawn(enemy_data: Dictionary):
	# Client receives dungeon enemy spawn data and spawns it
	print("GameWorld: Client received RPC to spawn dungeon enemy: type=", enemy_data.type, " pos=", enemy_data.position)
	
	if not multiplayer.is_server():
		# Convert enemy type to full path if needed (same as _spawn_enemies)
		var enemy_type = enemy_data.type
		if not enemy_type.begins_with("res://"):
			# If type is just "enemy_rat", convert to full path
			enemy_type = "res://scenes/" + enemy_type + ".tscn"
		
		var enemy_scene = load(enemy_type)
		if not enemy_scene:
			push_error("ERROR: Could not load enemy scene: ", enemy_type)
			return
		
		var entities_node = get_node_or_null("Entities")
		if not entities_node:
			push_error("ERROR: Could not find Entities node!")
			return
		
		# Check if enemy already exists (avoid duplicates from _spawn_enemies() + RPC)
		# Also check if enemy is dead or queued for deletion
		for child in entities_node.get_children():
			if child.is_in_group("enemy") and child.has_meta("dungeon_spawned"):
				# Check if it's a duplicate by position
				var child_pos = child.global_position
				if child_pos.distance_to(enemy_data.position) < 1.0: # Same position
					# Also check if it's dead - if so, remove it first
					if "is_dead" in child and child.is_dead:
						print("GameWorld: Removing dead duplicate enemy at ", enemy_data.position)
						child.queue_free()
						# Continue to spawn new one
					else:
						print("GameWorld: Enemy already exists at ", enemy_data.position, ", skipping duplicate spawn")
						return
		
		# Find the enemy index from the position in the enemies array
		# We need to match the server's enemy index to ensure consistent naming
		var enemy_index = -1
		if dungeon_data.has("enemies") and dungeon_data.enemies is Array:
			for idx in range(dungeon_data.enemies.size()):
				var e_data = dungeon_data.enemies[idx]
				if e_data.has("position") and e_data.position.distance_to(enemy_data.position) < 1.0:
					enemy_index = idx
					break
		
		# If we couldn't find the index, use a fallback
		if enemy_index == -1:
			enemy_index = entities_node.get_child_count()
		
		var enemy = enemy_scene.instantiate()
		enemy.name = "Enemy_%d" % enemy_index
		enemy.set_meta("dungeon_spawned", true)
		enemy.set_meta("enemy_index", enemy_index)
		
		# Store room reference for AI (if available and valid)
		if enemy_data.has("room") and enemy_data.room is Dictionary:
			enemy.set_meta("room", enemy_data.room)
		
		# Set multiplayer authority BEFORE adding to scene tree (CRITICAL!)
		if multiplayer.has_multiplayer_peer():
			enemy.set_multiplayer_authority(1)
			print("GameWorld: Set enemy authority to 1 BEFORE add_child (RPC): ", enemy.name)
		
		# Set enemy stats BEFORE adding to scene
		if "max_health" in enemy_data:
			enemy.max_health = enemy_data.max_health
		
		# If it's a humanoid enemy, set the humanoid_type
		if enemy_type.ends_with("enemy_humanoid.tscn") and "humanoid_type" in enemy_data:
			enemy.humanoid_type = enemy_data.humanoid_type
		if "move_speed" in enemy_data:
			enemy.move_speed = enemy_data.move_speed
		if "damage" in enemy_data:
			enemy.damage = enemy_data.damage
		
		# CRITICAL: Set collision mask BEFORE adding to scene to ensure enemies collide with walls (layer 7 = bit 6 = 64)
		# This overrides any collision_mask set in the scene file
		enemy.collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64)
		
		# Add to scene tree AFTER setting authority and stats
		entities_node.add_child(enemy)
		enemy.global_position = enemy_data.position
		
		# CRITICAL: Re-verify collision_mask AFTER adding to scene (in case scene file overrides it)
		# This ensures enemies always collide with walls (layer 7 = bit 6 = 64)
		if enemy.collision_mask != (1 | 2 | 64):
			print("GameWorld: WARNING - Enemy ", enemy.name, " collision_mask was ", enemy.collision_mask, ", expected ", (1 | 2 | 64), "! Correcting...")
			enemy.collision_mask = 1 | 2 | 64
		
		# Verify authority is still set
		if multiplayer.has_multiplayer_peer():
			var auth_after = enemy.get_multiplayer_authority()
			print("GameWorld: Enemy authority AFTER add_child (RPC): ", enemy.name, " authority: ", auth_after, " is_authority: ", enemy.is_multiplayer_authority())
			if auth_after != 1:
				push_error("GameWorld: ERROR - Enemy authority lost after add_child in RPC! Expected 1, got ", auth_after)
		
		print("GameWorld: Client spawned dungeon enemy via RPC: ", enemy.name, " at ", enemy_data.position, " (type: ", enemy_type, ") authority: ", enemy.get_multiplayer_authority())

func _sync_existing_chest_states_to_client(client_peer_id: int):
	# Sync chest open states to new client
	var entities_node = get_node_or_null("Entities")
	if not entities_node:
		return
	
	var opened_chest_count = 0
	for child in entities_node.get_children():
		if child.is_in_group("interactable_object"):
			if "object_type" in child and child.object_type == "Chest":
				if "is_chest_opened" in child and child.is_chest_opened:
					# Found an opened chest - sync it to the client
					var obj_name = child.name
					_sync_chest_state.rpc_id(client_peer_id, obj_name, true)
					opened_chest_count += 1
					print("GameWorld: Syncing opened chest ", obj_name, " to client ", client_peer_id)
	
	print("GameWorld: Synced ", opened_chest_count, " opened chests to client ", client_peer_id)

@rpc("authority", "reliable")
func _sync_chest_state(obj_name: String, is_opened: bool):
	# Client receives chest state sync
	if not multiplayer.is_server():
		var entities_node = get_node_or_null("Entities")
		if entities_node:
			var chest = entities_node.get_node_or_null(obj_name)
			if chest and "is_chest_opened" in chest:
				chest.is_chest_opened = is_opened
				if chest.has_method("_sync_chest_open"):
					# Call the sync function to update visuals (loot type doesn't matter for visual sync)
					chest._sync_chest_open("coin")
				elif "sprite" in chest and "chest_opened_frame" in chest:
					if chest.sprite and chest.chest_opened_frame >= 0:
						chest.sprite.frame = chest.chest_opened_frame
				print("GameWorld: Client received chest state sync for ", obj_name, " - opened: ", is_opened)

func _sync_existing_torches_to_client(client_peer_id: int):
	# Sync existing torches to newly connected client
	if dungeon_data.is_empty() or not dungeon_data.has("torches"):
		return
	
	var torches = dungeon_data.torches
	print("GameWorld: Syncing ", torches.size(), " torches to client ", client_peer_id)
	
	for torch_data in torches:
		_sync_torch_spawn.rpc_id(client_peer_id, torch_data.position, torch_data.rotation)

@rpc("authority", "reliable")
func _sync_torch_spawn(torch_position: Vector2, torch_rotation: float):
	# Clients spawn torch when server tells them to
	if not multiplayer.is_server():
		var torch_scene = preload("res://scenes/torch_wall.tscn")
		if not torch_scene:
			return
		
		var entities_node = get_node_or_null("Entities")
		if entities_node:
			var torch = torch_scene.instantiate()
			torch.name = "Torch_%d" % entities_node.get_child_count()
			entities_node.add_child(torch)
			torch.global_position = torch_position
			torch.rotation_degrees = torch_rotation
			print("Client spawned torch at ", torch_position, " with rotation ", torch_rotation)

func _clear_level():
	# Clear previous level data
	print("GameWorld: Clearing previous level...")
	
	# Clear tilemap layers - ensure we get references from scene if they exist
	var environment = get_node_or_null("Environment")
	if environment:
		var layer0 = environment.get_node_or_null("DungeonLayer0")
		if layer0:
			layer0.clear()
		var layer_above = environment.get_node_or_null("TileMapLayerAbove")
		if layer_above:
			layer_above.clear()
	
	# Also clear via stored references if they exist
	if dungeon_tilemap_layer:
		dungeon_tilemap_layer.clear()
	if dungeon_tilemap_layer_above:
		dungeon_tilemap_layer_above.clear()
	
	# Remove all entities EXCEPT players (enemies, torches, loot, etc.)
	var entities_node = get_node_or_null("Entities")
	if entities_node:
		var entities_to_remove = []
		for child in entities_node.get_children():
			# Don't free players - they persist across levels
			if not child.is_in_group("player"):
				entities_to_remove.append(child)
		
		# Free all entities immediately (not queue_free) to ensure they're gone before spawning new ones
		for entity in entities_to_remove:
			if is_instance_valid(entity):
				entity.free() # Use free() instead of queue_free() for immediate removal
	
	# Remove stairs area
	var stairs_area = get_node_or_null("StairsArea")
	if stairs_area:
		stairs_area.free() # Use free() for immediate removal
	
	# Clear dungeon data (but keep it for now until new one is generated)
	# dungeon_data = {}  # Don't clear yet, wait for new generation
	
	print("GameWorld: Previous level cleared")

func _move_all_players_to_start_room():
	# Move all players to the start room of the new level
	if dungeon_data.is_empty() or not dungeon_data.has("start_room"):
		return
	
	var start_room = dungeon_data.start_room
	_update_spawn_points(start_room)
	
	# Move all players to spawn points
	var players = get_tree().get_nodes_in_group("player")
	var spawn_index = 0
	for player in players:
		if spawn_index < player_manager.spawn_points.size():
			var new_pos = player_manager.spawn_points[spawn_index]
			player.global_position = new_pos
			print("GameWorld: Moved player ", player.name, " to start room at ", new_pos)
			spawn_index += 1
		else:
			# Fallback: place in center of start room
			var room_center_x = (start_room.x + start_room.w / 2.0) * 16
			var room_center_y = (start_room.y + start_room.h / 2.0) * 16
			player.global_position = Vector2(room_center_x, room_center_y)
			print("GameWorld: Moved player ", player.name, " to start room center at ", player.global_position)

func _create_stairs_area():
	# Remove existing stairs area if any
	var existing_stairs = get_node_or_null("StairsArea")
	if existing_stairs:
		existing_stairs.queue_free()
	
	# Check if stairs data exists
	if dungeon_data.is_empty() or not dungeon_data.has("stairs") or dungeon_data.stairs.is_empty():
		return
	
	var stairs_data = dungeon_data.stairs
	if not stairs_data.has("world_pos") or not stairs_data.has("world_size"):
		return
	
	# Create stairs Area2D programmatically
	var stairs_area = Area2D.new()
	stairs_area.name = "StairsArea"
	
	# Set collision layer/mask BEFORE adding to scene
	stairs_area.collision_layer = 0
	stairs_area.collision_mask = 1 # Detect players (layer 1)
	
	# Add script BEFORE adding to scene (so _ready() is called properly)
	var stairs_script = load("res://scripts/stairs.gd")
	if stairs_script:
		stairs_area.set_script(stairs_script)
	
	# Add collision shape
	var collision_shape = CollisionShape2D.new()
	var rect_shape = RectangleShape2D.new()
	rect_shape.size = stairs_data.world_size
	collision_shape.shape = rect_shape
	stairs_area.add_child(collision_shape)
	
	# Set position
	stairs_area.global_position = stairs_data.world_pos
	
	# Add to scene AFTER everything is set up
	add_child(stairs_area)
	
	print("GameWorld: Created stairs Area2D at ", stairs_data.world_pos, " with size ", stairs_data.world_size)

func _on_player_reached_stairs(player: Node):
	# Player reached stairs - trigger level complete
	if not multiplayer.is_server() and multiplayer.has_multiplayer_peer():
		return # Only server handles this
	
	# Prevent multiple triggers - if already triggered, ignore
	if level_complete_triggered:
		print("GameWorld: Level complete already triggered, ignoring duplicate trigger")
		return
	
	print("GameWorld: Player ", player.name, " reached stairs!")
	
	# Mark as triggered to prevent re-triggering
	level_complete_triggered = true
	
	# Disable controls and collision for the player who reached stairs
	var player_peer_id = player.get_multiplayer_authority() if player.has_method("get_multiplayer_authority") else 0
	player.controls_disabled = true
	# Remove collision layer (layer 1 = players)
	player.set_collision_layer_value(1, false)
	print("GameWorld: Disabled controls and collision for player ", player.name)
	
	# Show black fade overlay for server's local player if this is the server's player
	var my_peer_id = multiplayer.get_unique_id() if multiplayer.has_multiplayer_peer() else 1
	if player_peer_id == my_peer_id:
		_show_black_fade_overlay()
	
	# Sync controls disabled and collision removal to clients
	if multiplayer.has_multiplayer_peer() and player_peer_id > 0:
		_sync_player_exit_stairs.rpc(player_peer_id)
	
	# Drop any held objects for all players before level completion
	var entities_node = get_node_or_null("Entities")
	if entities_node:
		for child in entities_node.get_children():
			if child.is_in_group("player") and child.has_method("_force_drop_held_object"):
				child._force_drop_held_object()
	
	# Stop HUD timer when level completes
	var hud = get_node_or_null("IngameHUD")
	var level_time: float = 0.0
	if hud and hud.has_method("stop_timer"):
		hud.stop_timer()
		# Get the level time before stopping
		if hud.has_method("get_level_time"):
			level_time = hud.get_level_time()
	
	# Fade out player
	_fade_out_player(player)
	
	# Show level complete UI (server and clients) with per-player stats
	_show_level_complete_ui(level_time)
	# Sync to all clients (each client will show their own local player's stats)
	if multiplayer.has_multiplayer_peer():
		_sync_show_level_complete.rpc(level_time)
	
	# After delay, hide UI and generate new level
	await get_tree().create_timer(5.0).timeout # Show stats for 5 seconds
	
	# Hide level complete UI (server and clients)
	var level_complete_ui = get_node_or_null("LevelCompleteUI")
	if level_complete_ui:
		level_complete_ui.visible = false
	# Sync hide to all clients
	if multiplayer.has_multiplayer_peer():
		_sync_hide_level_complete.rpc()
	
	# Clear previous level
	_clear_level()
	
	# Wait for old entities to be fully freed before generating new level
	await get_tree().process_frame
	await get_tree().process_frame # Wait an extra frame to ensure cleanup is complete
	
	# Generate next level
	current_level += 1
	print("GameWorld: Incremented to level ", current_level, " (was level ", current_level - 1, ")")
	
	# Generate the new dungeon (this is async but we don't await it - it will complete in background)
	_generate_dungeon()
	
	# Wait for dungeon generation to complete (it has await statements inside)
	# We need to wait for all the async operations in _generate_dungeon() to finish
	await get_tree().process_frame
	await get_tree().process_frame
	await get_tree().process_frame # Extra frame to ensure everything is done
	
	# Verify current_level is still correct
	print("GameWorld: After dungeon generation, current_level = ", current_level)
	
	# Show level number (server and clients) - use the incremented level
	print("GameWorld: Showing level number for level ", current_level)
	_show_level_number()
	# Sync to all clients
	if multiplayer.has_multiplayer_peer():
		print("GameWorld: Syncing level number ", current_level, " to all clients")
		_sync_show_level_number.rpc(current_level)
	
	# Restart HUD timer for new level
	hud = get_node_or_null("IngameHUD")
	if hud and hud.has_method("start_timer"):
		hud.start_timer()
	
	# Restore controls and collision for all players (server side)
	_restore_player_controls_and_collision()
	
	# Sync restore to all clients
	if multiplayer.has_multiplayer_peer():
		_sync_restore_player_controls.rpc()
	
	# Remove black fade overlay (server and clients)
	_remove_black_fade_overlay()
	if multiplayer.has_multiplayer_peer():
		_sync_remove_black_fade.rpc()
	
	# Move all players to start room (server side)
	_move_all_players_to_start_room()
	
	# Fade players back in
	_fade_in_all_players()
	
	# Sync new level to all clients - use start room since all players should be there
	# IMPORTANT: Wait multiple frames to ensure dungeon generation and enemy spawning is complete before syncing
	await get_tree().process_frame
	await get_tree().process_frame # Wait extra frames to ensure enemies are fully initialized
	
	if multiplayer.has_multiplayer_peer():
		var start_room = dungeon_data.start_room if not dungeon_data.is_empty() and dungeon_data.has("start_room") else {}
		
		# Debug: Verify enemies are in dungeon_data before syncing
		if dungeon_data.has("enemies"):
			print("GameWorld: Server about to sync new level with ", dungeon_data.enemies.size(), " enemies to all clients")
		else:
			print("GameWorld: ERROR: Server dungeon_data has NO 'enemies' key when syncing new level!")
		
		_sync_dungeon.rpc(dungeon_data, dungeon_seed, current_level, start_room)

func _get_local_player_stats() -> Dictionary:
	# Get stats for the local player (for level complete screen)
	# Returns a dictionary with: enemies_defeated, coins_collected
	var stats = {
		"enemies_defeated": 0,
		"coins_collected": 0
	}
	
	# Find local player(s) - in multiplayer, find player with authority matching this client
	# In single-player, just use the first player
	var players = get_tree().get_nodes_in_group("player")
	var local_player = null
	
	if multiplayer.has_multiplayer_peer():
		# Multiplayer: find player with matching authority (client's own player)
		var my_peer_id = multiplayer.get_unique_id()
		for player in players:
			if player.has_method("get_multiplayer_authority") and player.get_multiplayer_authority() == my_peer_id:
				local_player = player
				break
	else:
		# Single-player: use first player
		if players.size() > 0:
			local_player = players[0]
	
	if local_player and local_player.character_stats:
		# Get enemies defeated (kills)
		if "kills" in local_player.character_stats:
			stats.enemies_defeated = local_player.character_stats.kills
		
		# Get coins collected
		if "coin" in local_player.character_stats:
			stats.coins_collected = local_player.character_stats.coin
	
	return stats

func _fade_out_player(player: Node):
	# Fade out all sprite layers
	var fade_tween = create_tween()
	fade_tween.set_parallel(true)
	
	var sprite_layers = []
	if "sprite_body" in player and player.sprite_body:
		sprite_layers.append(player.sprite_body)
	if "sprite_boots" in player and player.sprite_boots:
		sprite_layers.append(player.sprite_boots)
	if "sprite_armour" in player and player.sprite_armour:
		sprite_layers.append(player.sprite_armour)
	if "sprite_facial_hair" in player and player.sprite_facial_hair:
		sprite_layers.append(player.sprite_facial_hair)
	if "sprite_hair" in player and player.sprite_hair:
		sprite_layers.append(player.sprite_hair)
	if "sprite_eyes" in player and player.sprite_eyes:
		sprite_layers.append(player.sprite_eyes)
	if "sprite_eyelashes" in player and player.sprite_eyelashes:
		sprite_layers.append(player.sprite_eyelashes)
	if "sprite_addons" in player and player.sprite_addons:
		sprite_layers.append(player.sprite_addons)
	if "sprite_headgear" in player and player.sprite_headgear:
		sprite_layers.append(player.sprite_headgear)
	if "sprite_weapon" in player and player.sprite_weapon:
		sprite_layers.append(player.sprite_weapon)
	if "shadow" in player and player.shadow:
		sprite_layers.append(player.shadow)
	
	for sprite_layer in sprite_layers:
		if sprite_layer:
			fade_tween.tween_property(sprite_layer, "modulate:a", 0.0, 1.0)

func _fade_in_all_players():
	# Fade in all players after level transition
	var players = get_tree().get_nodes_in_group("player")
	for player in players:
		_fade_in_player(player)

func _fade_in_player(player: Node):
	# Fade in all sprite layers
	var fade_tween = create_tween()
	fade_tween.set_parallel(true)
	
	var sprite_layers = []
	if "sprite_body" in player and player.sprite_body:
		sprite_layers.append(player.sprite_body)
	if "sprite_boots" in player and player.sprite_boots:
		sprite_layers.append(player.sprite_boots)
	if "sprite_armour" in player and player.sprite_armour:
		sprite_layers.append(player.sprite_armour)
	if "sprite_facial_hair" in player and player.sprite_facial_hair:
		sprite_layers.append(player.sprite_facial_hair)
	if "sprite_hair" in player and player.sprite_hair:
		sprite_layers.append(player.sprite_hair)
	if "sprite_eyes" in player and player.sprite_eyes:
		sprite_layers.append(player.sprite_eyes)
	if "sprite_eyelashes" in player and player.sprite_eyelashes:
		sprite_layers.append(player.sprite_eyelashes)
	if "sprite_addons" in player and player.sprite_addons:
		sprite_layers.append(player.sprite_addons)
	if "sprite_headgear" in player and player.sprite_headgear:
		sprite_layers.append(player.sprite_headgear)
	if "sprite_weapon" in player and player.sprite_weapon:
		sprite_layers.append(player.sprite_weapon)
	if "shadow" in player and player.shadow:
		sprite_layers.append(player.shadow)
	
	for sprite_layer in sprite_layers:
		if sprite_layer:
			sprite_layer.modulate.a = 0.0 # Start invisible
			fade_tween.tween_property(sprite_layer, "modulate:a", 1.0, 1.0)

func _show_black_fade_overlay():
	# Create black fade overlay for player who reached exit
	# Remove existing fade if any
	var existing_fade = get_node_or_null("BlackFadeOverlay")
	if existing_fade:
		existing_fade.queue_free()
	
	# Create CanvasLayer with z_index 999 (below level complete UI which is 1000)
	var fade_layer = CanvasLayer.new()
	fade_layer.name = "BlackFadeOverlay"
	fade_layer.layer = 999  # Below level complete UI (1000) but above gameplay
	add_child(fade_layer)
	
	# Create ColorRect that fills the screen
	var fade_rect = ColorRect.new()
	fade_rect.name = "FadeRect"
	fade_rect.color = Color(0, 0, 0, 1)  # Black, fully opaque
	fade_rect.set_anchors_preset(Control.PRESET_FULL_RECT)  # Fill entire screen
	fade_layer.add_child(fade_rect)
	
	# Fade in from transparent to black
	fade_rect.modulate.a = 0.0  # Start transparent
	var fade_tween = create_tween()
	fade_tween.tween_property(fade_rect, "modulate:a", 1.0, 0.5)  # Fade in over 0.5 seconds
	print("GameWorld: Created black fade overlay for player who reached exit")

func _remove_black_fade_overlay():
	# Remove black fade overlay when new level starts
	var existing_fade = get_node_or_null("BlackFadeOverlay")
	if existing_fade:
		# Fade out quickly before removing
		var fade_rect = existing_fade.get_node_or_null("FadeRect")
		if fade_rect:
			var fade_tween = create_tween()
			fade_tween.tween_property(fade_rect, "modulate:a", 0.0, 0.2)  # Fade out over 0.2 seconds
			await fade_tween.finished
		existing_fade.queue_free()
		print("GameWorld: Removed black fade overlay")

func _restore_player_controls_and_collision():
	# Restore controls and collision for all players when new level starts
	var players = get_tree().get_nodes_in_group("player")
	for player in players:
		player.controls_disabled = false
		# Restore collision layer (layer 1 = players)
		player.set_collision_layer_value(1, true)
		print("GameWorld: Restored controls and collision for player ", player.name)

func _show_level_complete_ui(level_time: float = 0.0):
	# Create or show level complete UI
	var level_complete_ui = get_node_or_null("LevelCompleteUI")
	if not level_complete_ui:
		# Try to load scene if it exists, but fall back to programmatic creation if it doesn't
		var scene_path = "res://scenes/level_complete_ui.tscn"
		if ResourceLoader.exists(scene_path):
			var level_complete_scene = load(scene_path)
			if level_complete_scene:
				level_complete_ui = level_complete_scene.instantiate()
				level_complete_ui.name = "LevelCompleteUI"
				add_child(level_complete_ui)
			else:
				# Scene file exists but failed to load - fall back to programmatic creation
				print("GameWorld: Warning - level_complete_ui.tscn exists but failed to load, creating programmatically")
				level_complete_ui = _create_level_complete_ui_programmatically()
		else:
			# Scene file doesn't exist - create UI programmatically (expected behavior)
			level_complete_ui = _create_level_complete_ui_programmatically()
	
	if level_complete_ui:
		if level_complete_ui.has_method("show_stats"):
			# Get stats for local player
			var local_stats = _get_local_player_stats()
			# Use current_level directly (matches what HUD shows)
			# current_level hasn't been incremented yet when this is called
			level_complete_ui.show_stats(
				local_stats.enemies_defeated,
				0, # times_downed - not shown per user request
				0.0, # exp_collected - not shown per user request
				local_stats.coins_collected,
				level_time,
				current_level
			)

func _show_level_number():
	# Show level number text
	print("GameWorld: _show_level_number() called with current_level = ", current_level)
	var level_text_ui = get_node_or_null("LevelTextUI")
	if not level_text_ui:
		# Try to load scene if it exists, but fall back to programmatic creation if it doesn't
		var scene_path = "res://scenes/level_text_ui.tscn"
		if ResourceLoader.exists(scene_path):
			var level_text_scene = load(scene_path)
			if level_text_scene:
				level_text_ui = level_text_scene.instantiate()
				level_text_ui.name = "LevelTextUI"
				add_child(level_text_ui)
			else:
				# Scene file exists but failed to load - fall back to programmatic creation
				print("GameWorld: Warning - level_text_ui.tscn exists but failed to load, creating programmatically")
				level_text_ui = _create_level_text_ui_programmatically()
		else:
			# Scene file doesn't exist - create UI programmatically (expected behavior)
			level_text_ui = _create_level_text_ui_programmatically()
	
	if level_text_ui:
		if level_text_ui.has_method("show_level"):
			# Store the level number in a local variable to ensure we use the correct value
			var level_to_show = current_level
			print("GameWorld: Calling show_level(", level_to_show, ") on LevelTextUI (current_level = ", current_level, ")")
			# Make sure we pass the current level value explicitly
			level_text_ui.show_level(level_to_show)
		else:
			print("GameWorld: ERROR - LevelTextUI does not have show_level method!")
	else:
		print("GameWorld: ERROR - Could not create or find LevelTextUI!")

func _load_hud():
	# Check if HUD already exists - only load it once
	var existing_hud = get_node_or_null("IngameHUD")
	if existing_hud and is_instance_valid(existing_hud):
		print("GameWorld: HUD already exists, skipping load (will just reset timer)")
		# Reset timer for new level if method exists
		if existing_hud.has_method("reset_level_timer"):
			existing_hud.reset_level_timer()
		return
	
	# Load HUD dynamically to avoid scene loading errors
	# This is optional - don't crash if HUD scene doesn't exist or fails to load
	# Use a try-catch-like approach by checking for errors
	var hud_scene_path = "res://scenes/ingame_hud.tscn"
	
	# Check if scene exists
	if not ResourceLoader.exists(hud_scene_path):
		print("GameWorld: HUD scene not found at ", hud_scene_path, " - HUD disabled")
		return
	
	# Try to load the scene
	var hud_scene = load(hud_scene_path)
	if not hud_scene:
		print("GameWorld: Warning - Failed to load HUD scene from ", hud_scene_path)
		return
	
	# Try to instantiate
	var hud = null
	if hud_scene.has_method("instantiate"):
		hud = hud_scene.instantiate()
	else:
		print("GameWorld: Warning - HUD scene is not a PackedScene")
		return
	
	if not hud:
		print("GameWorld: Warning - Failed to instantiate HUD scene")
		return
	
	# Add to scene tree
	hud.name = "IngameHUD"
	
	# Ensure HUD is visible and on top layer
	hud.visible = true
	hud.layer = 100 # High layer to ensure HUD is on top of everything
	
	add_child(hud)
	
	# Reset timer if method exists
	if hud.has_method("reset_level_timer"):
		hud.reset_level_timer()
	
	print("GameWorld: HUD loaded successfully and added to scene tree")
	print("GameWorld: HUD visible: ", hud.visible, ", layer: ", hud.layer)

func _initialize_hud():
	# Find or get the HUD and reset its level timer
	# This is optional - don't crash if HUD doesn't exist
	var hud = get_node_or_null("IngameHUD")
	if hud and is_instance_valid(hud) and hud.has_method("reset_level_timer"):
		hud.reset_level_timer()
	else:
		print("GameWorld: HUD not found or not ready - this is OK if HUD scene is missing")

func _create_level_complete_ui_programmatically() -> Node:
	# Create level complete UI programmatically
	var canvas_layer = CanvasLayer.new()
	canvas_layer.name = "LevelCompleteUI"
	canvas_layer.layer = 1000  # Very high z_index so it appears above black fade
	add_child(canvas_layer)
	
	# Load standard font (as FontFile)
	var standard_font = load("res://assets/fonts/standard_font.png") as FontFile
	if not standard_font:
		print("GameWorld: Warning - Could not load standard_font.png as FontFile")
	
	# Create theme with standard font
	var theme = Theme.new()
	if standard_font:
		theme.default_font = standard_font
		theme.default_font_size = 10
	
	var vbox = VBoxContainer.new()
	vbox.theme = theme
	canvas_layer.add_child(vbox)
	
	# Center the VBoxContainer properly
	# Use PRESET_CENTER to anchor at center, then set offsets to center horizontally
	var screen_size = get_viewport().get_visible_rect().size
	vbox.set_anchors_preset(Control.PRESET_CENTER)
	# Set offsets so container is centered horizontally (equal left/right offsets)
	# and positioned vertically (offset from center)
	vbox.offset_left = - screen_size.x / 2
	vbox.offset_right = screen_size.x / 2
	vbox.offset_top = -200 # Position a bit up from center
	vbox.offset_bottom = screen_size.y / 2 - 200 # Balance to maintain vertical position
	vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
	
	# Title - "LEVEL COMPLETE!" in large size
	var title = Label.new()
	title.name = "TitleLabel"
	title.text = "LEVEL COMPLETE!"
	title.theme = theme
	title.add_theme_font_size_override("font_size", 72) # Large size
	title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	title.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
	vbox.add_child(title)
	
	# Stats header - "stats" in smaller size
	var stats_header = Label.new()
	stats_header.name = "StatsHeaderLabel"
	stats_header.text = "stats"
	stats_header.theme = theme
	stats_header.add_theme_font_size_override("font_size", 32) # Smaller than title
	stats_header.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	stats_header.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
	vbox.add_child(stats_header)
	
	# Stats container
	var stats_container = VBoxContainer.new()
	stats_container.theme = theme
	stats_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
	vbox.add_child(stats_container)
	
	# Stats labels - enemies defeated and level time
	var enemies_label = Label.new()
	enemies_label.name = "EnemiesLabel"
	enemies_label.theme = theme
	enemies_label.add_theme_font_size_override("font_size", 24)
	enemies_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	enemies_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
	stats_container.add_child(enemies_label)
	
	var time_label = Label.new()
	time_label.name = "TimeLabel"
	time_label.theme = theme
	time_label.add_theme_font_size_override("font_size", 24)
	time_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	time_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
	stats_container.add_child(time_label)
	
	var downed_label = Label.new()
	downed_label.name = "DownedLabel"
	downed_label.theme = theme
	downed_label.add_theme_font_size_override("font_size", 24)
	downed_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	downed_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
	stats_container.add_child(downed_label)
	
	var exp_label = Label.new()
	exp_label.name = "ExpLabel"
	exp_label.theme = theme
	exp_label.add_theme_font_size_override("font_size", 24)
	exp_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	exp_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
	stats_container.add_child(exp_label)
	
	var coins_label = Label.new()
	coins_label.name = "CoinsLabel"
	coins_label.theme = theme
	coins_label.add_theme_font_size_override("font_size", 24)
	coins_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	coins_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill available width
	stats_container.add_child(coins_label)
	
	# Add script
	var script = load("res://scripts/level_complete_ui.gd")
	if script:
		canvas_layer.set_script(script)
	
	return canvas_layer

func _create_level_text_ui_programmatically() -> Node:
	# Create level text UI programmatically
	var canvas_layer = CanvasLayer.new()
	canvas_layer.name = "LevelTextUI"
	add_child(canvas_layer)
	
	var vbox = VBoxContainer.new()
	vbox.set_anchors_preset(Control.PRESET_CENTER)
	canvas_layer.add_child(vbox)
	
	# Level label
	var level_label = Label.new()
	level_label.name = "LevelLabel"
	level_label.text = "LEVEL 1"
	level_label.add_theme_font_size_override("font_size", 64)
	level_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	vbox.add_child(level_label)
	
	# Add script
	var script = load("res://scripts/level_text_ui.gd")
	if script:
		canvas_layer.set_script(script)
	
	return canvas_layer

func _move_players_to_host_room(host_room: Dictionary):
	# Move any existing players to spawn points in the host's room
	if host_room.is_empty() or player_manager.spawn_points.is_empty():
		return
	
	# Get all players that belong to this client (local players)
	var my_peer_id = multiplayer.get_unique_id()
	var players_to_move = []
	for player in player_manager.get_all_players():
		# Only move players that belong to this client (local players)
		if player.peer_id == my_peer_id:
			players_to_move.append(player)
	
	if players_to_move.is_empty():
		return
	
	print("GameWorld: Moving ", players_to_move.size(), " local players to host room")
	
	# Move each player to a free spawn point
	var spawn_index = 0
	for player in players_to_move:
		if spawn_index < player_manager.spawn_points.size():
			var new_pos = player_manager.spawn_points[spawn_index]
			player.position = new_pos
			print("GameWorld: Moved player ", player.name, " to ", new_pos)
			spawn_index += 1

func _spawn_blocking_doors():
	# Spawn blocking doors from dungeon data
	if dungeon_data.is_empty() or not dungeon_data.has("blocking_doors"):
		return
	
	var blocking_doors = dungeon_data.blocking_doors
	if blocking_doors == null or not blocking_doors is Array:
		return
	
	var door_scene = load("res://scenes/door.tscn")
	if not door_scene:
		push_error("ERROR: Could not load door scene!")
		return
	
	var entities_node = get_node_or_null("Entities")
	if not entities_node:
		push_error("ERROR: Could not find Entities node!")
		return
	
	print("GameWorld: Spawning ", blocking_doors.size(), " blocking doors")
	
	# Track pillar placement per room to avoid duplicates
	var rooms_with_pillars: Dictionary = {} # Key: room string "x,y", Value: true if pillar exists
	
	for i in range(blocking_doors.size()):
		var door_data = blocking_doors[i]
		if not door_data is Dictionary:
			continue
		
		var door = door_scene.instantiate()
		door.name = "BlockingDoor_%d" % i
		door.add_to_group("blocking_door")
		
		# Set door properties BEFORE adding to scene (so _ready() has correct values)
		door.type = door_data.type if "type" in door_data else "StoneDoor"
		door.direction = door_data.direction if "direction" in door_data else "Up"
		door.is_closed = door_data.is_closed if "is_closed" in door_data else true
		
		# CRITICAL: Set puzzle requirements based on door_data
		if "puzzle_type" in door_data:
			if door_data.puzzle_type == "enemy":
				door.requires_enemies = true
				door.requires_switch = false
				print("GameWorld: Door ", door.name, " requires enemies to open (puzzle_type: enemy)")
			elif door_data.puzzle_type in ["switch_walk", "switch_pillar"]:
				door.requires_enemies = false
				door.requires_switch = true
				print("GameWorld: Door ", door.name, " requires switch to open (puzzle_type: ", door_data.puzzle_type, ")")
		door.blocking_room = door_data.blocking_room if "blocking_room" in door_data else {}
		door.switch_room = door_data.switch_room if "switch_room" in door_data else {}
		
		# CRITICAL: Verify door has blocking_room set - StoneDoor/GateDoor MUST be in a puzzle room
		if (door_data.type == "StoneDoor" or door_data.type == "GateDoor"):
			if not "blocking_room" in door_data or door_data.blocking_room.is_empty():
				push_error("GameWorld: ERROR - Blocking door ", door.name, " (", door_data.type, ") has no blocking_room! This door should not exist! Removing it.")
				door.queue_free()
				continue
			
			# CRITICAL: Verify door has puzzle_type - StoneDoor/GateDoor MUST have a puzzle
			if not "puzzle_type" in door_data:
				push_error("GameWorld: ERROR - Blocking door ", door.name, " (", door_data.type, ") has no puzzle_type! This door should not exist! Removing it.")
				door.queue_free()
				continue
			
			print("GameWorld: Creating blocking door ", door.name, " (", door_data.type, ") for puzzle room (", door_data.blocking_room.x, ", ", door_data.blocking_room.y, "), puzzle_type: ", door_data.puzzle_type)
		
		# CRITICAL: Store original door connection info from door_data
		# For blocking doors: room1 = puzzle room (where door is IN / leads FROM)
		#                     room2 = other room (where door leads TO)
		#                     blocking_room = puzzle room (same as room1, where puzzle is)
		# Use original_room1 and original_room2 from door_data (which may have been swapped)
		if "original_room1" in door_data and door_data.original_room1:
			door.room1 = door_data.original_room1 # This is always the puzzle room
		elif "door" in door_data and door_data.door is Dictionary:
			# Fallback to original door if original_room1 not set
			var original_door = door_data.door
			if "room1" in original_door and original_door.room1:
				door.room1 = original_door.room1
		
		if "original_room2" in door_data and door_data.original_room2:
			door.room2 = door_data.original_room2 # This is always the other room
		elif "door" in door_data and door_data.door is Dictionary:
			# Fallback to original door if original_room2 not set
			var original_door = door_data.door
			if "room2" in original_door and original_door.room2:
				door.room2 = original_door.room2
		
		# CRITICAL: For StoneDoor/GateDoor, verify door.room1 matches blocking_room
		# The door should be IN the puzzle room (room1 == blocking_room)
		if (door_data.type == "StoneDoor" or door_data.type == "GateDoor") and door.blocking_room and not door.blocking_room.is_empty():
			if not door.room1 or door.room1.is_empty():
				push_error("GameWorld: ERROR - Blocking door ", door.name, " has no room1! Cannot verify it's in puzzle room! Removing it.")
				door.queue_free()
				continue
			
			# Verify room1 (where door is) matches blocking_room (puzzle room)
			var room1_matches_blocking = (door.room1.x == door.blocking_room.x and \
										  door.room1.y == door.blocking_room.y and \
										  door.room1.w == door.blocking_room.w and \
										  door.room1.h == door.blocking_room.h)
			
			if not room1_matches_blocking:
				push_error("GameWorld: ERROR - Blocking door ", door.name, " room1 (", door.room1.x, ",", door.room1.y, ") doesn't match blocking_room (", door.blocking_room.x, ",", door.blocking_room.y, ")! This door is NOT in the puzzle room! Removing it.")
				door.queue_free()
				continue
			
			print("GameWorld: Blocking door ", door.name, " verified - room1 (", door.room1.x, ",", door.room1.y, ") == blocking_room (", door.blocking_room.x, ",", door.blocking_room.y, ") - door is IN puzzle room")
		
		# Set multiplayer authority BEFORE adding to scene
		if multiplayer.has_multiplayer_peer():
			door.set_multiplayer_authority(1)
		
		# CRITICAL: Set position BEFORE adding to scene tree (so _ready() can use it)
		door.global_position = door_data.position if "position" in door_data else Vector2.ZERO
		
		# Add to scene (this triggers _ready() which will use the position we just set)
		entities_node.add_child(door)
		
		# NOTE: Doors are connected to room triggers automatically by room_trigger._find_room_entities()
		# No need to manually connect them here
		
		# CRITICAL SAFETY CHECK: Verify door is for a puzzle room (StoneDoor/GateDoor should ONLY exist in puzzle rooms)
		if door_data.type == "StoneDoor" or door_data.type == "GateDoor":
			if not "blocking_room" in door_data or door_data.blocking_room.is_empty():
				push_error("GameWorld: ERROR - Blocking door ", door.name, " (", door_data.type, ") has no blocking_room! This door should not exist!")
				door.queue_free()
				continue
			
			if not "puzzle_type" in door_data:
				push_error("GameWorld: ERROR - Blocking door ", door.name, " (", door_data.type, ") has no puzzle_type! This door should not exist!")
				door.queue_free()
				continue
		
		# CRITICAL: Verify that this door actually has puzzle elements
		# Puzzle elements should already be created in dungeon_generator, but verify they exist
		var has_puzzle_element = false
		
		# Spawn floor switch if this door requires one (puzzle_type is "switch_walk" or "switch_pillar")
		if "puzzle_type" in door_data and (door_data.puzzle_type == "switch_walk" or door_data.puzzle_type == "switch_pillar"):
			if "floor_switch_position" in door_data or ("switch_data" in door_data and door_data.switch_data.has("position")):
				var switch_pos = door_data.floor_switch_position if "floor_switch_position" in door_data else door_data.switch_data.position
				var switch_tile_x = door_data.switch_tile_x if "switch_tile_x" in door_data else door_data.switch_data.tile_x
				var switch_tile_y = door_data.switch_tile_y if "switch_tile_y" in door_data else door_data.switch_data.tile_y
				var switch_type = door_data.switch_type if "switch_type" in door_data else ("walk" if door_data.puzzle_type == "switch_walk" else "pillar")
				var switch_weight = door_data.switch_required_weight if "switch_required_weight" in door_data else (1.0 if switch_type == "walk" else 5.0)
				
				# CRITICAL: Check if switch already exists for THIS SPECIFIC ROOM (to avoid duplicates)
				# Only connect to switches in the SAME blocking_room - never connect across rooms!
				var existing_switch = null
				var door_blocking_room = door_data.blocking_room if "blocking_room" in door_data else {}
				
				# CRITICAL: Verify door has valid blocking_room before searching for switches
				if door_blocking_room.is_empty():
					push_error("GameWorld: ERROR - Door ", door.name, " has empty blocking_room! Cannot find switches!")
					continue
				
				for existing in get_tree().get_nodes_in_group("floor_switch"):
					if not is_instance_valid(existing):
						continue
					
					# CRITICAL: Check ROOM FIRST (most important), then position
					# Switches MUST have switch_room metadata set when spawned
					if not existing.has_meta("switch_room"):
						continue # Switch has no room metadata - skip it (can't verify it's in the right room)
					
					var existing_switch_room = existing.get_meta("switch_room")
					if existing_switch_room.is_empty():
						continue # Invalid room data
					
					# CRITICAL: Verify switch is in the SAME room as door (check room FIRST)
					var room_match = (existing_switch_room.x == door_blocking_room.x and \
									  existing_switch_room.y == door_blocking_room.y and \
									  existing_switch_room.w == door_blocking_room.w and \
									  existing_switch_room.h == door_blocking_room.h)
					
					if not room_match:
						# Switch is in a different room - DO NOT connect, skip it
						continue
					
					# Room matches - now check position (must be exact match)
					var pos_match = existing.global_position.distance_to(switch_pos) < 1.0
					if pos_match:
						# Both room AND position match - this is the correct switch
						existing_switch = existing
						print("GameWorld: Found existing switch ", existing.name, " in room (", door_blocking_room.x, ",", door_blocking_room.y, ") at position ", existing.global_position, " matching door room and position")
						break
				
				if existing_switch:
					# CRITICAL: Double-check room match before connecting
					var existing_switch_room_final = existing_switch.get_meta("switch_room")
					var final_room_match = false
					if existing_switch_room_final and not existing_switch_room_final.is_empty() and door_blocking_room and not door_blocking_room.is_empty():
						final_room_match = (existing_switch_room_final.x == door_blocking_room.x and \
											existing_switch_room_final.y == door_blocking_room.y and \
											existing_switch_room_final.w == door_blocking_room.w and \
											existing_switch_room_final.h == door_blocking_room.h)
					
					if final_room_match:
						# Switch already exists in the SAME room - connect door to existing switch
						door.connected_switches.append(existing_switch)
						has_puzzle_element = true
						print("GameWorld: Connected door ", door.name, " (room: ", door_blocking_room.get("x", "?"), ",", door_blocking_room.get("y", "?"), ") to existing switch ", existing_switch.name, " in SAME room")
						
						# If this is a pillar switch, ensure a pillar exists in the room
						# Check if switch is a pillar switch (check both door_data and existing switch)
						var is_pillar_switch = (switch_type == "pillar")
						if not is_pillar_switch and "switch_type" in existing_switch:
							is_pillar_switch = (existing_switch.switch_type == "pillar")
						
						if is_pillar_switch:
							# Check if we've already verified/placed a pillar for this room
							var room_key = str(door_blocking_room.x) + "," + str(door_blocking_room.y)
							if not rooms_with_pillars.has(room_key):
								# First time checking this room - check if pillar exists
								var pillar_exists_in_room = false
								for obj in get_tree().get_nodes_in_group("interactable_object"):
									if is_instance_valid(obj):
										var obj_type = obj.object_type if "object_type" in obj else ""
										if obj_type == "Pillar":
											if obj.has_meta("room"):
												var obj_room = obj.get_meta("room")
												if obj_room and not obj_room.is_empty():
													if obj_room.x == door_blocking_room.x and obj_room.y == door_blocking_room.y and \
													   obj_room.w == door_blocking_room.w and obj_room.h == door_blocking_room.h:
														pillar_exists_in_room = true
														print("GameWorld: Found existing pillar in room (", door_blocking_room.x, ",", door_blocking_room.y, ")")
														break
								
								# If no pillar exists, place one
								if not pillar_exists_in_room:
									print("GameWorld: Pillar switch found but no pillar in room (", door_blocking_room.x, ",", door_blocking_room.y, ") - placing pillar now")
									_place_pillar_in_room(door_blocking_room, existing_switch.global_position)
									# Mark room as checked after attempting to place pillar
									# Note: Even if placement fails, mark as checked to avoid repeated attempts
									rooms_with_pillars[room_key] = true
								else:
									# Pillar exists - mark room as checked
									rooms_with_pillars[room_key] = true
					else:
						push_error("GameWorld: ERROR - Attempted to connect door ", door.name, " to switch ", existing_switch.name, " but rooms don't match! Door room: (", door_blocking_room.get("x", "?"), ",", door_blocking_room.get("y", "?"), "), Switch room: (", existing_switch_room_final.get("x", "?") if existing_switch_room_final else "?", ",", existing_switch_room_final.get("y", "?") if existing_switch_room_final else "?", ")")
						# Don't connect - spawn a new switch instead
						existing_switch = null
				else:
					# Spawn new switch - CRITICAL: Only spawn if we have valid room data
					if not door_blocking_room or door_blocking_room.is_empty():
						push_error("GameWorld: ERROR - Cannot spawn switch for door ", door.name, " - no blocking_room!")
						continue
					
					# CRITICAL: Verify switch position matches door_data switch position exactly
					# If switch_room in door_data doesn't match blocking_room, it's an error
					if "switch_room" in door_data:
						var door_switch_room = door_data.switch_room
						if door_switch_room and not door_switch_room.is_empty():
							var switch_room_matches = (door_switch_room.x == door_blocking_room.x and \
													   door_switch_room.y == door_blocking_room.y and \
													   door_switch_room.w == door_blocking_room.w and \
													   door_switch_room.h == door_blocking_room.h)
							if not switch_room_matches:
								push_error("GameWorld: ERROR - Door ", door.name, " switch_room (", door_switch_room.x, ",", door_switch_room.y, ") doesn't match blocking_room (", door_blocking_room.x, ",", door_blocking_room.y, ")! This is a bug!")
								door.queue_free()
								continue
					
					var switch = _spawn_floor_switch(switch_pos, switch_weight, switch_tile_x, switch_tile_y, switch_type, door_blocking_room)
					if switch:
						# CRITICAL: Verify switch has room metadata set (should be set in _spawn_floor_switch)
						if not switch.has_meta("switch_room"):
							push_error("GameWorld: ERROR - Switch ", switch.name, " was spawned without switch_room metadata! Setting it now as fallback.")
							switch.set_meta("switch_room", door_blocking_room) # Set it now as fallback
						
						# CRITICAL: Verify switch room matches door blocking_room before connecting
						# This ensures switches are ONLY connected to doors in the SAME room
						var switch_room_check = switch.get_meta("switch_room")
						if switch_room_check and not switch_room_check.is_empty() and door_blocking_room and not door_blocking_room.is_empty():
							var room_match_before_connect = (switch_room_check.x == door_blocking_room.x and \
															 switch_room_check.y == door_blocking_room.y and \
															 switch_room_check.w == door_blocking_room.w and \
															 switch_room_check.h == door_blocking_room.h)
							
							if room_match_before_connect:
								# Connect switch to door ONLY if rooms match exactly
								door.connected_switches.append(switch)
								has_puzzle_element = true
								print("GameWorld: Spawned switch ", switch.name, " in room (", door_blocking_room.x, ",", door_blocking_room.y, ") and connected to door ", door.name, " in SAME room")
								
								# If this is a pillar switch, place a pillar in the same room
								if switch_type == "pillar":
									# Check if we've already placed a pillar for this room
									var room_key = str(door_blocking_room.x) + "," + str(door_blocking_room.y)
									if not rooms_with_pillars.has(room_key):
										print("GameWorld: Placing pillar for new pillar switch in room (", door_blocking_room.x, ",", door_blocking_room.y, ")")
										_place_pillar_in_room(door_blocking_room, switch_pos)
										# Mark room as checked after attempting to place pillar
										# Note: Even if placement fails, mark as checked to avoid repeated attempts
										rooms_with_pillars[room_key] = true
									else:
										print("GameWorld: Pillar already exists/placed for room (", door_blocking_room.x, ",", door_blocking_room.y, ") - skipping placement")
							else:
								push_error("GameWorld: ERROR - Switch ", switch.name, " room (", switch_room_check.x, ",", switch_room_check.y, ") doesn't match door blocking_room (", door_blocking_room.x, ",", door_blocking_room.y, ")! NOT connecting! Removing switch.")
								switch.queue_free() # Remove the switch since it's in wrong room
								has_puzzle_element = false # Don't count this as puzzle element
						else:
							push_error("GameWorld: ERROR - Switch ", switch.name, " or door ", door.name, " has invalid room data! Switch room: ", switch_room_check, ", Door room: ", door_blocking_room)
							switch.queue_free() # Remove invalid switch
							has_puzzle_element = false
					else:
						push_warning("GameWorld: WARNING - Failed to spawn floor switch for door ", door.name, "!")
		
		# Place key in room if this is a KeyDoor
		if door_data.type == "KeyDoor" and "key_room" in door_data:
			_place_key_in_room(door_data.key_room)
			has_puzzle_element = true # KeyDoors are always valid
		
		# Spawn enemy spawners if this door requires enemies (puzzle_type is "enemy")
		if "puzzle_type" in door_data and door_data.puzzle_type == "enemy":
			print("GameWorld: ===== Door ", door.name, " has puzzle_type 'enemy' - checking for enemy_spawners =====")
			if "enemy_spawners" in door_data and door_data.enemy_spawners is Array:
				print("GameWorld: Door has enemy_spawners array with ", door_data.enemy_spawners.size(), " spawners")
				var spawner_created = false
				for spawner_data in door_data.enemy_spawners:
					if spawner_data is Dictionary and spawner_data.has("position"):
						# Check if spawner already exists for this room (to avoid duplicates)
						var existing_spawner = null
						for existing in get_tree().get_nodes_in_group("enemy_spawner"):
							if existing.global_position.distance_to(spawner_data.position) < 1.0:
								existing_spawner = existing
								break
						
						if existing_spawner:
							# Spawner already exists - just verify it's set up correctly
							existing_spawner.set_meta("blocking_room", door_data.blocking_room)
							spawner_created = true
							print("GameWorld: Found existing spawner ", existing_spawner.name, " for door ", door.name)
						else:
							# Spawn new spawner
							var spawner = _spawn_enemy_spawner(
								spawner_data.position,
								spawner_data.room if spawner_data.has("room") else door_data.blocking_room,
								spawner_data # Pass spawner_data to access spawn_once flag
							)
							if spawner:
								# Store reference to door for spawner (optional - spawner will be found by room trigger)
								spawner.set_meta("blocking_room", door_data.blocking_room)
								spawner_created = true
								print("GameWorld: Spawned enemy spawner ", spawner.name, " for door ", door.name, " at ", spawner_data.position)
				if spawner_created:
					has_puzzle_element = true
			else:
				push_warning("GameWorld: WARNING - Failed to spawn enemy spawner for door ", door.name, "!")
				if "enemy_spawners" not in door_data:
					push_warning("GameWorld:   Reason: door_data has no 'enemy_spawners' key!")
				elif not door_data.enemy_spawners is Array:
					push_warning("GameWorld:   Reason: door_data.enemy_spawners is not an Array! Type: ", typeof(door_data.enemy_spawners))
				elif door_data.enemy_spawners.size() == 0:
					push_warning("GameWorld:   Reason: door_data.enemy_spawners array is empty!")
		else:
			if "puzzle_type" in door_data:
				print("GameWorld: Door ", door.name, " has puzzle_type '", door_data.puzzle_type, "' (not 'enemy')")
		
		# CRITICAL: If door has no puzzle elements (neither switch nor spawner), this is an error
		# This should never happen if dungeon_generator logic is correct, but add safety check
		if door_data.type != "KeyDoor" and not has_puzzle_element:
			push_error("GameWorld: ERROR - Blocking door ", door.name, " (type: ", door_data.type, ") has no puzzle elements! This door should not exist!")
			print("GameWorld: Door data keys: ", door_data.keys())
			print("GameWorld: Door puzzle_type: ", door_data.get("puzzle_type", "MISSING"))
			print("GameWorld: Door has requires_switch: ", door_data.get("requires_switch", false))
			print("GameWorld: Door has requires_enemies: ", door_data.get("requires_enemies", false))
			print("GameWorld: Door has floor_switch_position: ", "floor_switch_position" in door_data)
			print("GameWorld: Door has enemy_spawners: ", "enemy_spawners" in door_data)
			# Remove the door since it's invalid - it was created without puzzle elements
			door.queue_free()
			print("GameWorld: Removed invalid blocking door ", door.name, " - it had no puzzle elements!")
			continue # Skip to next door
	
	print("GameWorld: Spawned ", blocking_doors.size(), " blocking doors")

func _spawn_floor_switch(i_position: Vector2, required_weight: float, tile_x: int, tile_y: int, switch_type: String = "walk", switch_room: Dictionary = {}) -> Node:
	# Spawn a floor switch using the scene file
	# switch_type: "walk" for walk-on switch (weight 1), "pillar" for pillar switch (weight 5)
	var switch_scene = load("res://scenes/floor_switch.tscn")
	if not switch_scene:
		push_error("ERROR: Could not load floor_switch scene!")
		return null
	
	var switch = switch_scene.instantiate()
	if not switch:
		push_error("ERROR: Could not instantiate floor_switch scene!")
		return null
	
	switch.name = "FloorSwitch_%d_%d" % [tile_x, tile_y]
	switch.add_to_group("floor_switch")
	
	# Set properties
	switch.switch_type = switch_type if switch_type == "walk" or switch_type == "pillar" else "walk"
	switch.required_weight = required_weight # Will be overridden in _ready() based on switch_type, but set it here too
	switch.switch_tile_position = Vector2i(tile_x, tile_y)
	
	# Set multiplayer authority
	if multiplayer.has_multiplayer_peer():
		switch.set_multiplayer_authority(1)
	
	# CRITICAL: Store switch_room metadata BEFORE adding to scene
	# This ensures switches can be matched to doors in the same room
	if switch_room and not switch_room.is_empty():
		switch.set_meta("switch_room", switch_room)
		print("GameWorld: Set switch_room metadata for switch - room (", switch_room.x, ", ", switch_room.y, ")")
	else:
		push_warning("GameWorld: WARNING - Spawning switch without switch_room metadata! This may cause cross-room connections!")
	
	# Add to scene
	var entities_node = get_node_or_null("Entities")
	if entities_node:
		entities_node.add_child(switch)
		switch.global_position = i_position
		
		# Update tilemap to show switch tile (initial inactive state)
		if dungeon_tilemap_layer:
			var initial_tile: Vector2i
			if switch_type == "pillar":
				initial_tile = Vector2i(16, 9) # Pillar switch inactive
			else:
				initial_tile = Vector2i(11, 9) # Walk-on switch inactive
			dungeon_tilemap_layer.set_cell(Vector2i(tile_x, tile_y), 0, initial_tile)
		
		print("GameWorld: Spawned ", switch_type, " floor switch at ", i_position, " tile (", tile_x, ", ", tile_y, "), room: (", switch_room.get("x", "?") if switch_room and not switch_room.is_empty() else "?", ", ", switch_room.get("y", "?") if switch_room and not switch_room.is_empty() else "?", ")")
		return switch
	
	return null

func _spawn_enemy_spawner(i_position: Vector2, room: Dictionary, spawner_data: Dictionary = {}) -> Node:
	# Spawn an enemy spawner for a blocking room
	var spawner_script = load("res://scripts/enemy_spawner.gd")
	if not spawner_script:
		push_error("ERROR: Could not load enemy_spawner script!")
		return null
	
	var spawner = Node2D.new()
	spawner.set_script(spawner_script)
	spawner.name = "EnemySpawner_%d_%d" % [room.x, room.y] if room and not room.is_empty() else "EnemySpawner_%d_%d" % [int(i_position.x), int(i_position.y)]
	spawner.add_to_group("enemy_spawner")
	
	# Set spawner properties - IMPORTANT: spawn_on_ready = false so enemies only spawn when player enters room
	spawner.spawn_on_ready = false # Don't spawn on ready - wait for room trigger
	spawner.respawn_time = 0.0 # Don't respawn - enemies spawn once when entering room
	spawner.max_enemies = 1 # One enemy per spawner
	
	# Check if this spawner should be destroyed after spawning once
	if spawner_data.has("spawn_once") and spawner_data.spawn_once:
		spawner.set_meta("spawn_once", true) # Mark spawner for destruction after spawning
	
	# Set enemy scenes (use default enemy types)
	# enemy_scenes is Array[PackedScene], so we need to properly type it
	var enemy_scenes: Array[PackedScene] = []
	var scene_paths = [
		"res://scenes/enemy_rat.tscn",
		"res://scenes/enemy_humanoid.tscn",
		"res://scenes/enemy_slime.tscn",
		"res://scenes/enemy_bat.tscn"
	]
	
	# Load scenes and add to typed array
	for path in scene_paths:
		var scene = load(path) as PackedScene
		if scene:
			enemy_scenes.append(scene)
	
	spawner.enemy_scenes = enemy_scenes
	
	# Set multiplayer authority
	if multiplayer.has_multiplayer_peer():
		spawner.set_multiplayer_authority(1)
	
	# Store room reference
	if room and not room.is_empty():
		spawner.set_meta("room", room)
	
	# Add to scene
	var entities_node = get_node_or_null("Entities")
	if entities_node:
		entities_node.add_child(spawner)
		spawner.global_position = i_position
		print("GameWorld: ✓✓✓ Successfully spawned enemy spawner '", spawner.name, "' at ", i_position, " for room at (", room.x if room and not room.is_empty() else "unknown", ", ", room.y if room and not room.is_empty() else "unknown", ")")
		print("GameWorld:   Spawner has room metadata: ", spawner.has_meta("room"))
		if spawner.has_meta("room"):
			var spawner_room = spawner.get_meta("room")
			print("GameWorld:   Spawner room metadata: (", spawner_room.x if spawner_room and not spawner_room.is_empty() else "none", ", ", spawner_room.y if spawner_room and not spawner_room.is_empty() else "none", ", ", spawner_room.w if spawner_room and not spawner_room.is_empty() else "none", "x", spawner_room.h if spawner_room and not spawner_room.is_empty() else "none", ")")
		print("GameWorld:   Spawner in group 'enemy_spawner': ", spawner.is_in_group("enemy_spawner"))
		print("GameWorld:   Spawner enemy_scenes.size(): ", spawner.enemy_scenes.size() if "enemy_scenes" in spawner else "N/A")
		return spawner
	
	return null

func _spawn_room_triggers():
	# Spawn room trigger areas for all rooms
	if dungeon_data.is_empty() or not dungeon_data.has("rooms"):
		return
	
	var rooms = dungeon_data.rooms
	if rooms == null or not rooms is Array:
		return
	
	var trigger_script = load("res://scripts/room_trigger.gd")
	if not trigger_script:
		push_error("ERROR: Could not load room_trigger script!")
		return
	
	var entities_node = get_node_or_null("Entities")
	if not entities_node:
		push_error("ERROR: Could not find Entities node!")
		return
	
	print("GameWorld: Spawning ", rooms.size(), " room triggers")
	
	var triggers_spawned = 0
	for i in range(rooms.size()):
		var room = rooms[i]
		if not room is Dictionary:
			print("GameWorld: WARNING - Room at index ", i, " is not a Dictionary, skipping")
			continue
		
		var trigger = Area2D.new()
		trigger.set_script(trigger_script)
		trigger.name = "RoomTrigger_%d" % i
		trigger.add_to_group("room_trigger")
		
		# Set room data
		trigger.room = room
		
		# Create collision shape covering ONLY the room interior (no overlap with adjacent rooms)
		var collision_shape = CollisionShape2D.new()
		var rect_shape = RectangleShape2D.new()
		var tile_size = 16
		# Room interior is from room.x + 2 to room.x + room.w - 2 (excluding 2-tile walls)
		# This ensures the trigger only covers THIS room, not adjacent rooms or doorways
		var room_world_x = (room.x + 2) * tile_size
		var room_world_y = (room.y + 2) * tile_size
		var room_world_w = (room.w - 4) * tile_size # Width excluding 2-tile walls on each side
		var room_world_h = (room.h - 4) * tile_size # Height excluding 2-tile walls on each side
		rect_shape.size = Vector2(room_world_w, room_world_h)
		collision_shape.shape = rect_shape
		# Position collision shape at center of room (relative to Area2D)
		collision_shape.position = Vector2(room_world_w / 2.0, room_world_h / 2.0)
		trigger.add_child(collision_shape)
		
		# Set Area2D global position to the top-left corner of the room interior
		# This ensures the trigger ONLY covers this specific room
		trigger.global_position = Vector2(room_world_x, room_world_y)
		
		# Set multiplayer authority
		if multiplayer.has_multiplayer_peer():
			trigger.set_multiplayer_authority(1)
		
		# Add to scene
		entities_node.add_child(trigger)
		triggers_spawned += 1
		print("GameWorld: Added room trigger ", trigger.name, " for room (", room.x, ", ", room.y, ") - ", triggers_spawned, "/", rooms.size())
	
	print("GameWorld: Spawned ", triggers_spawned, " room triggers (out of ", rooms.size(), " rooms)")

func _place_key_in_room(room: Dictionary):
	# Place a key in the specified room (as loot)
	if room.is_empty():
		return
	
	var loot_scene = preload("res://scenes/loot.tscn")
	if not loot_scene:
		return
	
	var entities_node = get_node_or_null("Entities")
	if not entities_node:
		return
	
	# Find a valid floor position in the room
	var tile_size = 16
	var valid_positions = []
	
	# Room interior is from room.x + 2 to room.x + room.w - 2
	for x in range(room.x + 2, room.x + room.w - 2):
		for y in range(room.y + 2, room.y + room.h - 2):
			if x >= 0 and x < dungeon_data.map_size.x and y >= 0 and y < dungeon_data.map_size.y:
				if dungeon_data.grid[x][y] == 1: # Floor
					var world_x = x * tile_size + tile_size / 2.0
					var world_y = y * tile_size + tile_size / 2.0
					valid_positions.append(Vector2(world_x, world_y))
	
	if valid_positions.size() > 0:
		# Use deterministic seed for key placement (ensures same position on host and clients)
		var rng = RandomNumberGenerator.new()
		var key_seed = dungeon_seed + (room.x * 1000) + (room.y * 100) + 5000  # Offset to avoid collisions with other objects
		rng.seed = key_seed
		var key_pos = valid_positions[rng.randi() % valid_positions.size()]
		
		# Spawn key loot
		var key_loot = loot_scene.instantiate()
		key_loot.name = "KeyLoot_%d_%d" % [int(key_pos.x), int(key_pos.y)]
		key_loot.loot_type = key_loot.LootType.KEY
		
		# Set multiplayer authority
		if multiplayer.has_multiplayer_peer():
			key_loot.set_multiplayer_authority(1)
		
		entities_node.add_child(key_loot)
		key_loot.global_position = key_pos
		
		print("GameWorld: Placed key in room at ", key_pos)

func _place_pillar_in_room(room: Dictionary, switch_position: Vector2):
	# Place a pillar in the specified room (needed for pillar switches)
	if room.is_empty():
		return
	
	var interactable_object_scene = load("res://scenes/interactable_object.tscn")
	if not interactable_object_scene:
		push_error("ERROR: Could not load interactable_object scene for pillar!")
		return
	
	var entities_node = get_node_or_null("Entities")
	if not entities_node:
		push_error("ERROR: Could not find Entities node for pillar placement!")
		return
	
	# Find a valid floor position in the room (away from the switch)
	var tile_size = 16
	var valid_positions = []
	
	# Room interior floor tiles: from room.x + 2 to room.x + room.w - 3 (excluding 2-tile walls on each side)
	# Floor tiles are at: room.x+2, room.x+3, ..., room.x+room.w-3 (last floor tile before right wall)
	# CRITICAL: Also exclude last row/column of floor tiles to prevent objects from spawning in walls
	# Objects are 16x16, so we need at least 1 tile buffer from walls
	# Floor tiles are at: room.x+2, room.x+3, ..., room.x+room.w-3 (last floor tile before wall)
	# To exclude last tile, we want: room.x+2, room.x+3, ..., room.x+room.w-4
	var min_x = room.x + 2
	var max_x = room.x + room.w - 4  # Exclude last 2 floor tiles (room.w-3 is last floor, room.w-4 is second-to-last)
	var min_y = room.y + 2
	var max_y = room.y + room.h - 4  # Exclude last 2 floor tiles (room.h-3 is last floor, room.h-4 is second-to-last)
	
	var interior_width = room.w - 4  # Exclude 2-tile walls on each side
	var interior_height = room.h - 4  # Exclude 2-tile walls on each side
	print("GameWorld: _place_pillar_in_room - Searching for valid positions in room (", room.x, ",", room.y, ",", room.w, "x", room.h, ")")
	print("GameWorld:   Interior size: ", interior_width, "x", interior_height, ", Checking tiles: x[", min_x, " to ", max_x, "], y[", min_y, " to ", max_y, "]")
	print("GameWorld:   Switch position: ", switch_position)
	
	for x in range(min_x, max_x + 1): # +1 because range is exclusive at end
		for y in range(min_y, max_y + 1):
			if x >= 0 and x < dungeon_data.map_size.x and y >= 0 and y < dungeon_data.map_size.y:
				if dungeon_data.grid[x][y] == 1: # Floor
					# CRITICAL: Interactable objects are 16x16 pixels with origin at (0,0) (top-left)
					# To center a 16x16 sprite in a 16x16 tile, we need to offset by 8 pixels into the tile
					# Tile (x,y) spans from (x*16, y*16) to ((x+1)*16, (y+1)*16)
					# Position sprite 8 pixels into the tile from top-left: (x*16 + 8, y*16 + 8)
					var world_x = x * tile_size + 8
					var world_y = y * tile_size + 8
					var world_pos = Vector2(world_x, world_y)
					
					# Ensure pillar is at least 1 tile away from the switch
					var distance_to_switch = world_pos.distance_to(switch_position)
					if distance_to_switch >= tile_size * 1: # At least 1 tiles away
						valid_positions.append(world_pos)
						print("GameWorld:   Valid position found at (", x, ",", y, ") -> world (", world_x, ",", world_y, "), distance to switch: ", distance_to_switch)
					else:
						print("GameWorld:   Position at (", x, ",", y, ") -> world (", world_x, ",", world_y, ") too close to switch (distance: ", distance_to_switch, " < ", tile_size, ")")
	
	print("GameWorld:   Found ", valid_positions.size(), " valid positions for pillar")
	if valid_positions.size() > 0:
		# Pick a deterministic random position using dungeon seed
		# This ensures server and clients place pillars in the same positions
		var rng = RandomNumberGenerator.new()
		# Use dungeon seed + room position + switch position as seed for deterministic randomness
		var pillar_seed = dungeon_seed + (room.x * 1000) + (room.y * 100) + int(switch_position.x) + int(switch_position.y)
		rng.seed = pillar_seed
		var pillar_pos = valid_positions[rng.randi() % valid_positions.size()]
		
		# Spawn pillar interactable object
		var pillar = interactable_object_scene.instantiate()
		pillar.name = "Pillar_%d_%d" % [int(pillar_pos.x), int(pillar_pos.y)]
		pillar.set_meta("dungeon_spawned", true)
		pillar.set_meta("room", room)
		
		# Set multiplayer authority
		if multiplayer.has_multiplayer_peer():
			pillar.set_multiplayer_authority(1)
		
		# Add to scene tree
		entities_node.add_child(pillar)
		pillar.global_position = pillar_pos
		
		# Call setup function to configure as pillar
		if pillar.has_method("setup_pillar"):
			pillar.call("setup_pillar")
		else:
			push_error("ERROR: Pillar does not have setup_pillar method!")
		
		# Add to group for easy access
		pillar.add_to_group("interactable_object")
		
		print("GameWorld: Placed pillar in room at ", pillar_pos, " (switch at ", switch_position, ")")
	else:
		push_warning("GameWorld: Could not find valid position for pillar in room! Room might be too small.")

func _connect_door_to_room_trigger(door: Node):
	# Connect a door to its room trigger area
	# blocking_room is a variable in door.gd, so it should exist
	var blocking_room = door.blocking_room
	if not blocking_room or blocking_room.is_empty():
		return
	
	# Find the room trigger for this room
	for trigger in get_tree().get_nodes_in_group("room_trigger"):
		if is_instance_valid(trigger):
			# room is a variable in room_trigger.gd, compare by values
			var trigger_room = trigger.room
			if trigger_room and not trigger_room.is_empty() and \
			   trigger_room.x == blocking_room.x and trigger_room.y == blocking_room.y and \
			   trigger_room.w == blocking_room.w and trigger_room.h == blocking_room.h:
				# Connect door to trigger
				door.room_trigger_area = trigger
				# Add door to trigger's doors list (doors_in_room is a variable in room_trigger.gd)
				trigger.doors_in_room.append(door)
				break
