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) # Create chat UI _create_chat_ui() # 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) # Send join message to chat (only on server to avoid duplicates) if multiplayer.is_server(): _send_player_join_message(peer_id, 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, player_info: Dictionary): print("GameWorld: Player disconnected - ", peer_id) # Send disconnect message to chat (only on server to avoid duplicates) if multiplayer.is_server(): _send_player_disconnect_message(peer_id, player_info) 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 (random for all levels) if dungeon_seed == 0: dungeon_seed = randi() # 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_ 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_ 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_chat_ui(): # Load chat UI scene var chat_ui_scene = load("res://scenes/chat_ui.tscn") if not chat_ui_scene: push_error("GameWorld: Could not load chat_ui.tscn scene!") return var chat_ui = chat_ui_scene.instantiate() if chat_ui: add_child(chat_ui) print("GameWorld: Chat UI scene instantiated and added to scene tree") else: push_error("GameWorld: Failed to instantiate chat_ui.tscn!") func _send_player_join_message(peer_id: int, player_info: Dictionary): # Send a chat message when a player joins # Only send from server to avoid duplicate messages if not multiplayer.is_server(): return # Get player name (use first player name from the player_info) var player_names = player_info.get("player_names", []) var player_name = "" if player_names.size() > 0: player_name = player_names[0] else: player_name = "Player%d" % peer_id # Get chat UI var chat_ui = get_node_or_null("ChatUI") if chat_ui and chat_ui.has_method("send_system_message"): var message = "%s joined the game" % player_name chat_ui.send_system_message(message) func _send_player_disconnect_message(peer_id: int, player_info: Dictionary): # Send a chat message when a player disconnects # Only send from server to avoid duplicate messages if not multiplayer.is_server(): return # Get player name from player_info var player_name = "" var player_names = player_info.get("player_names", []) if player_names.size() > 0: player_name = player_names[0] else: player_name = "Player%d" % peer_id # Get chat UI var chat_ui = get_node_or_null("ChatUI") if chat_ui and chat_ui.has_method("send_system_message"): var message = "%s left/disconnected" % player_name chat_ui.send_system_message(message) 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) # Center horizontally and position higher up var screen_size = get_viewport().get_visible_rect().size vbox.offset_left = -screen_size.x / 2 vbox.offset_right = screen_size.x / 2 vbox.offset_top = -250 # Position higher up from center vbox.offset_bottom = screen_size.y / 2 - 250 vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to fill width for proper centering 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 level_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Expand to center properly # Load standard_font.png as bitmap font var standard_font_resource = null if ResourceLoader.exists("res://assets/fonts/standard_font.png"): standard_font_resource = load("res://assets/fonts/standard_font.png") if standard_font_resource: level_label.add_theme_font_override("font", standard_font_resource) 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