extends RefCounted # Dungeon Generator for RPG DUNGEON VOL 3 tilemap # Tiles are 16x16 pixels # Walls are 32x32 pixels (2x2 tiles) # Ground is 1 tile (16x16) # Tile coordinates for RPG DUNGEON VOL 3 # Coordinates are (x, y) where x is column, y is row in the tileset # Tiles are 16x16 pixels each # Room wall tiles (outer walls) # Walls are 2 tiles tall (upper and lower parts) or 2 tiles wide (left and right parts) # Corner tiles are 2x2 (4 tiles total) # Top-left corner (2x2): (0,0), (1,0), (0,1), (1,1) const WALL_TOP_LEFT_TOP_LEFT = Vector2i(5, 0) # Top-left corner, top-left part const WALL_TOP_LEFT_TOP_RIGHT = Vector2i(1, 0) # Top-left corner, top-right part const WALL_TOP_LEFT_BOTTOM_LEFT = Vector2i(0, 1) # Top-left corner, bottom-left part const WALL_TOP_LEFT_BOTTOM_RIGHT = Vector2i(1, 1) # Top-left corner, bottom-right part # Top-right corner (2x2): (3,0), (4,0), (3,1), (4,1) const WALL_TOP_RIGHT_TOP_LEFT = Vector2i(3, 0) # Top-right corner, top-left part const WALL_TOP_RIGHT_TOP_RIGHT = Vector2i(4, 0) # Top-right corner, top-right part const WALL_TOP_RIGHT_BOTTOM_LEFT = Vector2i(3, 1) # Top-right corner, bottom-left part const WALL_TOP_RIGHT_BOTTOM_RIGHT = Vector2i(4, 1) # Top-right corner, bottom-right part # Top wall (1x2 tile - spans 2 tiles tall) const WALL_TOP_UPPER = Vector2i(2, 0) # Top wall, upper part const WALL_TOP_LOWER = Vector2i(2, 1) # Top wall, lower part # Left wall (2 tiles wide - spans 2 tiles horizontally) const WALL_LEFT_LEFT = Vector2i(0, 2) # Left wall, left part const WALL_LEFT_RIGHT = Vector2i(1, 2) # Left wall, right part # Right wall (2 tiles wide - spans 2 tiles horizontally) const WALL_RIGHT_LEFT = Vector2i(3, 2) # Right wall, left part const WALL_RIGHT_RIGHT = Vector2i(4, 2) # Right wall, right part # Bottom-left corner (2x2): (0,3), (1,3), (0,4), (1,4) const WALL_BOTTOM_LEFT_TOP_LEFT = Vector2i(0, 3) # Bottom-left corner, top-left part const WALL_BOTTOM_LEFT_TOP_RIGHT = Vector2i(1, 3) # Bottom-left corner, top-right part const WALL_BOTTOM_LEFT_BOTTOM_LEFT = Vector2i(0, 4) # Bottom-left corner, bottom-left part const WALL_BOTTOM_LEFT_BOTTOM_RIGHT = Vector2i(1, 4) # Bottom-left corner, bottom-right part # Bottom-right corner (2x2): (3,3), (4,3), (3,4), (4,4) const WALL_BOTTOM_RIGHT_TOP_LEFT = Vector2i(3, 3) # Bottom-right corner, top-left part const WALL_BOTTOM_RIGHT_TOP_RIGHT = Vector2i(4, 3) # Bottom-right corner, top-right part const WALL_BOTTOM_RIGHT_BOTTOM_LEFT = Vector2i(3, 4) # Bottom-right corner, bottom-left part const WALL_BOTTOM_RIGHT_BOTTOM_RIGHT = Vector2i(4, 4) # Bottom-right corner, bottom-right part # Bottom wall (1x2 tile - spans 2 tiles tall) const WALL_BOTTOM_UPPER = Vector2i(2, 3) # Bottom wall, upper part const WALL_BOTTOM_LOWER = Vector2i(2, 4) # Bottom wall, lower part # Inner wall tiles (for non-rectangular rooms) const INNER_WALL_TOP_LEFT = Vector2i(1, 6) const INNER_WALL_TOP_RIGHT = Vector2i(3, 6) const INNER_WALL_BOTTOM_LEFT = Vector2i(1, 8) const INNER_WALL_BOTTOM_RIGHT = Vector2i(3, 8) # Door tiles const DOOR_UP_START = Vector2i(7, 0) # 3x2 large const DOOR_LEFT_START = Vector2i(5, 2) # 2x3 large const DOOR_RIGHT_START = Vector2i(10, 2) # 2x3 large const DOOR_BOTTOM_START = Vector2i(7, 5) # 3x2 large # Stairs tiles (exit room) - similar to doors but different middle frame tile const STAIRS_UP_START = Vector2i(7, 0) # 3x2 large, middle tile is (10,0) instead of (8,0) const STAIRS_LEFT_START = Vector2i(5, 2) # 2x3 large, middle tile is (5,1) instead of (5,3) const STAIRS_RIGHT_START = Vector2i(10, 2) # 2x3 large, middle tile is (11,1) instead of (11,3) const STAIRS_DOWN_START = Vector2i(7, 5) # 3x2 large, middle tile is (6,6) instead of (8,6) # Ground/floor tiles (random selection) const FLOOR_TILES = [Vector2i(9, 8), Vector2i(14, 8), Vector2i(6, 11)] # Room generation parameters # Minimum room size is 3x3 floor tiles, but rooms need 2-tile walls on each side # So minimum room size including walls is 3 + 2*2 = 7 tiles (3 floor + 2 walls on each side) # But we want at least 3x3 floor, so room size should be at least 7x7 total const MIN_ROOM_SIZE = 7 # Minimum room size in tiles (includes walls, so 3x3 floor minimum) const MAX_ROOM_SIZE = 12 # Maximum room size in tiles const MIN_HOLE_SIZE = 9 # Minimum hole size in rooms (9x9 tiles) const DOOR_MIN_WIDTH = 3 # Minimum width for door frames const DOOR_MAX_WIDTH = 5 # Maximum width for door frames const CORRIDOR_WIDTH = 1 # Corridor width in tiles (1 tile) # Main generation function func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -> Dictionary: var rng = RandomNumberGenerator.new() if seed_value > 0: rng.seed = seed_value else: rng.randomize() # Calculate target room count based on level # Level 1: 7-8 rooms, then increase by 2-3 rooms per level var target_room_count = 7 + (level - 1) * 2 + rng.randi_range(0, 1) # Level 1: 7-8, Level 2: 9-10, etc. LogManager.log("DungeonGenerator: Level " + str(level) + " - Target room count: " + str(target_room_count), LogManager.CATEGORY_DUNGEON) # Initialize grid (0 = wall, 1 = floor, 2 = door, 3 = corridor) var grid = [] var tile_grid = [] # Actual tile coordinates (Vector2i) for rendering for x in range(map_size.x): grid.append([]) tile_grid.append([]) for y in range(map_size.y): grid[x].append(0) # Start with all walls tile_grid[x].append(Vector2i(0, 0)) # Default wall tile (will be set properly later) var all_rooms = [] var all_doors = [] # 1. Create first room at a random position var first_w = rng.randi_range(MIN_ROOM_SIZE, MAX_ROOM_SIZE) var first_h = rng.randi_range(MIN_ROOM_SIZE, MAX_ROOM_SIZE) var first_room = { "x": rng.randi_range(4, map_size.x - first_w - 4), "y": rng.randi_range(4, map_size.y - first_h - 4), "w": first_w, "h": first_h, "modifiers": [] } _set_floor(first_room, grid, tile_grid, map_size, rng) all_rooms.append(first_room) # 2. Try to place rooms until we reach target count or can't fit any more var attempts = 1000 while attempts > 0 and all_rooms.size() < target_room_count and all_rooms.size() > 0: var source_room = all_rooms[rng.randi() % all_rooms.size()] var new_room = _try_place_room_near(source_room, grid, map_size, rng) if new_room.w > 0: _set_floor(new_room, grid, tile_grid, map_size, rng) all_rooms.append(new_room) attempts -= 1 LogManager.log("DungeonGenerator: Generated " + str(all_rooms.size()) + " rooms (target was " + str(target_room_count) + ")", LogManager.CATEGORY_DUNGEON) # 3. Connect rooms with corridors/doors if all_rooms.size() > 1: _connect_rooms(all_rooms, grid, tile_grid, map_size, all_doors, rng) # 4. Add random holes in some rooms (minimum 9x9 tiles) for room in all_rooms: if rng.randf() < 0.3: # 30% chance for a hole _add_hole_to_room(room, grid, tile_grid, map_size, rng) # 5. Mark start room (random room for variety) var start_room_index = rng.randi() % all_rooms.size() var exit_room_index = -1 # Declare exit_room_index early to avoid scope issues all_rooms[start_room_index].modifiers.append({"type": "START"}) # 6. Mark exit room (farthest REACHABLE room from start) # First find all reachable rooms from start var reachable_rooms = _find_reachable_rooms(all_rooms[start_room_index], all_rooms, all_doors) LogManager.log("DungeonGenerator: Found " + str(reachable_rooms.size()) + " reachable rooms from start (out of " + str(all_rooms.size()) + " total)", LogManager.CATEGORY_DUNGEON) # CRITICAL: Remove inaccessible rooms (rooms not reachable from start) # Store the start room before filtering (it should always be reachable) var start_room_ref = all_rooms[start_room_index] var inaccessible_count = 0 # Create new array with only reachable rooms # Use value-based comparison (x, y, w, h) to check if room is reachable var filtered_rooms = [] for room in all_rooms: var is_reachable = false # Check if this room is in the reachable_rooms list by comparing values for reachable_room in reachable_rooms: if reachable_room.x == room.x and reachable_room.y == room.y and \ reachable_room.w == room.w and reachable_room.h == room.h: is_reachable = true break if is_reachable: filtered_rooms.append(room) else: inaccessible_count += 1 LogManager.log("DungeonGenerator: Removing inaccessible room at (" + str(room.x) + ", " + str(room.y) + ") - no corridor connection", LogManager.CATEGORY_DUNGEON) # Update all_rooms to only include reachable rooms all_rooms = filtered_rooms if inaccessible_count > 0: LogManager.log("DungeonGenerator: Removed " + str(inaccessible_count) + " inaccessible room(s). Remaining rooms: " + str(all_rooms.size()), LogManager.CATEGORY_DUNGEON) # Update start_room_index after filtering (find start room in new array using value-based comparison) start_room_index = -1 for i in range(all_rooms.size()): var room = all_rooms[i] if room.x == start_room_ref.x and room.y == start_room_ref.y and \ room.w == start_room_ref.w and room.h == start_room_ref.h: start_room_index = i break if start_room_index == -1: LogManager.log_error("DungeonGenerator: ERROR - Start room was removed! This should never happen!", LogManager.CATEGORY_DUNGEON) start_room_index = 0 # Fallback # Also remove doors connected to inaccessible rooms (clean up all_doors) var filtered_doors = [] var doors_removed = 0 for door in all_doors: var door_room1_reachable = false var door_room2_reachable = false # Check if door's connected rooms are in the filtered reachable rooms list (all_rooms now only contains reachable rooms) # Compare rooms by properties (x, y, w, h) since dictionary comparison might not work # Check both room1 and room2 against all reachable rooms if "room1" in door and door.room1 is Dictionary and not door.room1.is_empty(): for room in all_rooms: if door.room1.x == room.x and door.room1.y == room.y and \ door.room1.w == room.w and door.room1.h == room.h: door_room1_reachable = true break # Found room1, no need to keep checking if "room2" in door and door.room2 is Dictionary and not door.room2.is_empty(): for room in all_rooms: if door.room2.x == room.x and door.room2.y == room.y and \ door.room2.w == room.w and door.room2.h == room.h: door_room2_reachable = true break # Found room2, no need to keep checking # Only keep doors that connect two reachable rooms if door_room1_reachable and door_room2_reachable: filtered_doors.append(door) else: doors_removed += 1 LogManager.log("DungeonGenerator: Removing door - room1 reachable: " + str(door_room1_reachable) + ", room2 reachable: " + str(door_room2_reachable), LogManager.CATEGORY_DUNGEON) all_doors = filtered_doors if doors_removed > 0: LogManager.log("DungeonGenerator: Removed " + str(doors_removed) + " door(s) connected to inaccessible rooms. Remaining doors: " + str(all_doors.size()), LogManager.CATEGORY_DUNGEON) # Find the farthest reachable room (now all rooms are reachable, but find farthest) # Make sure we have at least 2 rooms (start and exit must be different) # exit_room_index is already declared at function level if all_rooms.size() < 2: LogManager.log_error("DungeonGenerator: ERROR - Not enough reachable rooms! Need at least 2 (start + exit), but only have " + str(all_rooms.size()), LogManager.CATEGORY_DUNGEON) # Use start room as exit if only one room exists (shouldn't happen, but handle gracefully) if all_rooms.size() == 1: exit_room_index = 0 else: # No rooms at all - this is a critical error LogManager.log_error("DungeonGenerator: CRITICAL ERROR - No rooms left after filtering!", LogManager.CATEGORY_DUNGEON) return {} # Return empty dungeon else: exit_room_index = _find_farthest_room(all_rooms, start_room_index) # Make sure exit room is different from start room if exit_room_index == start_room_index and all_rooms.size() > 1: # If exit is same as start, find second farthest var max_distance = 0 var second_farthest = -1 for i in range(all_rooms.size()): if i == start_room_index: continue var distance = abs(all_rooms[i].x - all_rooms[start_room_index].x) + abs(all_rooms[i].y - all_rooms[start_room_index].y) if distance > max_distance: max_distance = distance second_farthest = i if second_farthest != -1: exit_room_index = second_farthest all_rooms[exit_room_index].modifiers.append({"type": "EXIT"}) LogManager.log("DungeonGenerator: Selected exit room at index " + str(exit_room_index) + " position: " + str(all_rooms[exit_room_index].x) + "," + str(all_rooms[exit_room_index].y), LogManager.CATEGORY_DUNGEON) # 7. Render walls around rooms _render_room_walls(all_rooms, grid, tile_grid, map_size, rng) # 7.5. Place stairs in exit room BEFORE placing torches (so torches don't overlap stairs) var stairs_data = _place_stairs_in_exit_room(all_rooms[exit_room_index], grid, tile_grid, map_size, all_doors, rng) if stairs_data.is_empty(): LogManager.log_error("DungeonGenerator: ERROR - Failed to place stairs in exit room! Room size: " + str(all_rooms[exit_room_index].w) + "x" + str(all_rooms[exit_room_index].h) + " Doors: " + str(all_doors.size()), LogManager.CATEGORY_DUNGEON) # CRITICAL: Force place stairs - we MUST have an exit! LogManager.log("DungeonGenerator: FORCING stairs placement in exit room center", LogManager.CATEGORY_DUNGEON) stairs_data = _force_place_stairs(all_rooms[exit_room_index], grid, tile_grid, map_size, all_doors, rng) if stairs_data.is_empty(): LogManager.log_error("DungeonGenerator: CRITICAL ERROR - Could not place stairs even with force placement!", LogManager.CATEGORY_DUNGEON) # 8. Place torches in rooms (AFTER stairs, so torches don't overlap stairs) var all_torches = [] for room in all_rooms: var room_torches = _place_torches_in_room(room, grid, all_doors, map_size, rng) all_torches.append_array(room_torches) # 11. Place blocking doors on existing tile doors (after everything else is created) # IMPORTANT: This must happen BEFORE placing enemies, so we know which rooms have monster spawner puzzles var blocking_doors_result = _place_blocking_doors(all_rooms, all_doors, grid, map_size, rng, start_room_index, exit_room_index) var blocking_doors = blocking_doors_result.doors if blocking_doors_result.has("doors") else blocking_doors_result var room_puzzle_data = blocking_doors_result.puzzle_data if blocking_doors_result.has("puzzle_data") else {} # Extract rooms with monster spawner puzzles (these should NOT have pre-spawned enemies) var rooms_with_spawner_puzzles = [] var blocking_doors_array = blocking_doors if blocking_doors is Array else blocking_doors.doors for door_data in blocking_doors_array: if "puzzle_type" in door_data and door_data.puzzle_type == "enemy": if "blocking_room" in door_data and not door_data.blocking_room.is_empty(): var puzzle_room = door_data.blocking_room # Check if this room is already in the list (avoid duplicates) var already_in_list = false for existing_room in rooms_with_spawner_puzzles: if existing_room.x == puzzle_room.x and existing_room.y == puzzle_room.y and \ existing_room.w == puzzle_room.w and existing_room.h == puzzle_room.h: already_in_list = true break if not already_in_list: rooms_with_spawner_puzzles.append(puzzle_room) LogManager.log("DungeonGenerator: Room (" + str(puzzle_room.x) + ", " + str(puzzle_room.y) + ") has monster spawner puzzle - will skip pre-spawning enemies", LogManager.CATEGORY_DUNGEON) # 9. Place enemies in rooms (scaled by level, excluding start and exit rooms, and rooms with spawner puzzles) var all_enemies = [] for i in range(all_rooms.size()): var room = all_rooms[i] # Skip start room and exit room if i != start_room_index and i != exit_room_index: # CRITICAL: Skip rooms that have monster spawner puzzles (these will spawn enemies when player enters) var has_spawner_puzzle = false for spawner_room in rooms_with_spawner_puzzles: if spawner_room.x == room.x and spawner_room.y == room.y and \ spawner_room.w == room.w and spawner_room.h == room.h: has_spawner_puzzle = true LogManager.log("DungeonGenerator: Skipping pre-spawned enemies for room (" + str(room.x) + ", " + str(room.y) + ") - has monster spawner puzzle", LogManager.CATEGORY_DUNGEON) break if not has_spawner_puzzle: var room_enemies = _place_enemies_in_room(room, grid, map_size, rng, level) all_enemies.append_array(room_enemies) # 9.5. Place interactable objects in rooms (excluding start and exit rooms) var all_interactable_objects = [] for i in range(all_rooms.size()): var room = all_rooms[i] # Skip start room and exit room if i != start_room_index and i != exit_room_index: var room_objects = _place_interactable_objects_in_room(room, grid, map_size, all_doors, all_enemies, rng, room_puzzle_data) all_interactable_objects.append_array(room_objects) # NOTE: Stairs placement was moved earlier (step 7.5, before torches) to prevent torch overlap # NOTE: Blocking doors placement was moved earlier (step 11, before enemy placement) to identify spawner puzzle rooms return { "rooms": all_rooms, "doors": all_doors, "torches": all_torches, "enemies": all_enemies, "interactable_objects": all_interactable_objects, "stairs": stairs_data, "blocking_doors": blocking_doors, "grid": grid, "tile_grid": tile_grid, "map_size": map_size, "start_room": all_rooms[start_room_index], "exit_room": all_rooms[exit_room_index] } func _set_floor(room: Dictionary, grid: Array, tile_grid: Array, map_size: Vector2i, rng: RandomNumberGenerator): # Set floor tiles for the room (interior only, walls will be set separately) # Leave 2 tile border for walls (walls are 2 tiles tall) 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: grid[x][y] = 1 # Floor # Random floor tile variation var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()] tile_grid[x][y] = floor_tile func _try_place_room_near(source_room: Dictionary, grid: Array, map_size: Vector2i, rng: RandomNumberGenerator) -> Dictionary: var attempts = 20 while attempts > 0: var w = rng.randi_range(MIN_ROOM_SIZE, MAX_ROOM_SIZE) var h = rng.randi_range(MIN_ROOM_SIZE, MAX_ROOM_SIZE) # Try all four sides of the source room var sides = ["N", "S", "E", "W"] sides.shuffle() for side in sides: var x = source_room.x var y = source_room.y match side: "N": x = source_room.x + rng.randi_range(0, max(1, source_room.w - w)) y = source_room.y - h - 4 # 4 tiles gap "S": x = source_room.x + rng.randi_range(0, max(1, source_room.w - w)) y = source_room.y + source_room.h + 4 "W": x = source_room.x - w - 4 y = source_room.y + rng.randi_range(0, max(1, source_room.h - h)) "E": x = source_room.x + source_room.w + 4 y = source_room.y + rng.randi_range(0, max(1, source_room.h - h)) var new_room = {"x": x, "y": y, "w": w, "h": h, "modifiers": []} if _is_valid_room_position(new_room, grid, map_size): return new_room attempts -= 1 return {"x": 0, "y": 0, "w": 0, "h": 0, "modifiers": []} func _is_valid_room_position(room: Dictionary, grid: Array, map_size: Vector2i) -> bool: # Check bounds with buffer if room.x < 4 or room.y < 4 or room.x + room.w >= map_size.x - 4 or room.y + room.h >= map_size.y - 4: return false # Check if room overlaps with existing rooms (with 4-tile buffer) for x in range(room.x - 4, room.x + room.w + 4): for y in range(room.y - 4, room.y + room.h + 4): if x >= 0 and x < map_size.x and y >= 0 and y < map_size.y: if grid[x][y] != 0: # Not empty (wall) return false return true func _connect_rooms(all_rooms: Array, grid: Array, tile_grid: Array, map_size: Vector2i, all_doors: Array, rng: RandomNumberGenerator): # First pass: connect each room to closest neighbors var connected_rooms = {} for room in all_rooms: connected_rooms[room] = [] for room in all_rooms: var closest_rooms = _find_closest_rooms(room, all_rooms) var connection_attempts = 0 var max_connections = 3 for target_room in closest_rooms: if connection_attempts >= max_connections: break if not _rooms_are_connected(room, target_room, all_doors): var door = _create_corridor_between_rooms(room, target_room, grid, tile_grid, map_size, all_rooms, rng) if door.size() > 0: all_doors.append(door) connected_rooms[room].append(target_room) connected_rooms[target_room].append(room) connection_attempts += 1 # Second pass: ensure all rooms are connected var attempts = 100 while attempts > 0: var reachable = _find_reachable_rooms(all_rooms[0], all_rooms, all_doors) if reachable.size() == all_rooms.size(): break # Find unreachable room and connect it for room in all_rooms: if not reachable.has(room): for target_room in reachable: var door = _create_corridor_between_rooms(room, target_room, grid, tile_grid, map_size, all_rooms, rng) if door.size() > 0: all_doors.append(door) break break attempts -= 1 func _create_corridor_between_rooms(room1: Dictionary, room2: Dictionary, grid: Array, tile_grid: Array, map_size: Vector2i, all_rooms: Array, rng: RandomNumberGenerator) -> Dictionary: var dx = abs(room2.x - room1.x) var dy = abs(room2.y - room1.y) # Check if rooms are too far apart if dx > 20 or dy > 20: return {} # Helper function to check if a corridor path intersects any room's floor area except room1 and room2 # Corridors should not pass through the floor area of other rooms (walls are OK) var corridor_intersects_other_room = func(corridor_start_x: int, corridor_start_y: int, corridor_end_x: int, corridor_end_y: int, is_horizontal: bool) -> bool: if is_horizontal: # Horizontal corridor: check all tiles from start_x to end_x at y = corridor_y var corridor_y = corridor_start_y for check_x in range(min(corridor_start_x, corridor_end_x), max(corridor_start_x, corridor_end_x) + 1): # Check if this tile is inside any room's floor area (except room1 and room2) for other_room in all_rooms: if other_room == room1 or other_room == room2: continue # Check if the corridor tile is inside this room's floor area (excluding walls) # Floor area starts at room.x+2 and room.y+2 (after 2-tile wall) and ends before walls if check_x >= other_room.x + 2 and check_x < other_room.x + other_room.w - 2 and \ corridor_y >= other_room.y + 2 and corridor_y < other_room.y + other_room.h - 2: return true else: # Vertical corridor: check all tiles from start_y to end_y at x = corridor_x var corridor_x = corridor_start_x for check_y in range(min(corridor_start_y, corridor_end_y), max(corridor_start_y, corridor_end_y) + 1): # Check if this tile is inside any room's floor area (except room1 and room2) for other_room in all_rooms: if other_room == room1 or other_room == room2: continue # Check if the corridor tile is inside this room's floor area (excluding walls) # Floor area starts at room.x+2 and room.y+2 (after 2-tile wall) and ends before walls if corridor_x >= other_room.x + 2 and corridor_x < other_room.x + other_room.w - 2 and \ check_y >= other_room.y + 2 and check_y < other_room.y + other_room.h - 2: return true return false if dx > dy: # Horizontal corridor var left_room = room1 if room1.x < room2.x else room2 var right_room = room2 if room1.x < room2.x else room1 if right_room.x - (left_room.x + left_room.w) > 20: return {} # Door position - IN THE MIDDLE OF THE WALL (replacing part of the wall) # For horizontal doors (left/right), door is 2x3 tiles (2 wide, 3 tall) # The wall is 2 tiles wide (at left_room.x + left_room.w - 2 and left_room.x + left_room.w - 1) # Door must be at least 1 tile away from corners (corners are 2x2, so start at y+2) var wall_x = left_room.x + left_room.w - 1 # Right wall of left room (wall is at room.x + room.w - 1) var min_y = max(left_room.y + 2, right_room.y + 2) # At least 1 tile from top corner (corner is 2 tiles) var max_y = min(left_room.y + left_room.h - 5, right_room.y + right_room.h - 5) # At least 1 tile from bottom corner, and room for 3-tile tall door if max_y < min_y: return {} var door_y = min_y + rng.randi_range(0, max(0, max_y - min_y)) var door_width = rng.randi_range(DOOR_MIN_WIDTH, DOOR_MAX_WIDTH) var corridor_length = right_room.x - (left_room.x + left_room.w) # Check if corridor path would intersect any other rooms var corridor_start_x = wall_x + 1 var corridor_end_x = wall_x + corridor_length var corridor_y = door_y + 1 if corridor_intersects_other_room.call(corridor_start_x, corridor_y, corridor_end_x, corridor_y, true): return {} # Corridor would pass through another room, skip this connection # Create corridor (1 tile wide) - use floor tiles # Corridor is between the rooms, after the door for x in range(wall_x + 1, wall_x + corridor_length + 1): # Corridor starts after the wall if x >= 0 and x < map_size.x and door_y + 1 >= 0 and door_y + 1 < map_size.y: grid[x][door_y + 1] = 3 # Corridor (middle row of door) var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()] tile_grid[x][door_y + 1] = floor_tile # Create door on RIGHT wall of left room (2x3 tiles - 2 wide, 3 tall) # Door is placed ON the wall, replacing the 2-tile wide wall # The wall is at wall_x (left_room.x + left_room.w - 1) and wall_x - 1 (left_room.x + left_room.w - 2) # This is the RIGHT wall of the left room, so use DOOR_RIGHT_START (10,2 to 11,4) var door_start_x = wall_x - 1 # Door starts at the left wall tile (2 tiles wide) var door_start_y = door_y # Start from door_y position var door_tile_start = DOOR_RIGHT_START # Use RIGHT door for right wall (2x3: 10,2 to 11,4) for door_dx in range(2): # Door is 2 tiles wide for door_dy in range(3): # Door is 3 tiles tall var x = door_start_x + door_dx var y = door_start_y + door_dy if x >= 0 and x < map_size.x and y >= 0 and y < map_size.y: grid[x][y] = 2 # Door area (replaces wall) # Use door tile coordinates (10,2) + offset for 2x3 door tile_grid[x][y] = door_tile_start + Vector2i(door_dx, door_dy) if door_dx == 0 and door_dy == 1: grid[x][y] = 1 # Floor var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()] tile_grid[x][y] = floor_tile # Also create door on LEFT wall of right room (if there's a gap) if corridor_length > 0: var right_wall_x = right_room.x # Left wall of right room var right_door_start_x = right_wall_x # Door starts at the left wall tile (2 tiles wide) var right_door_tile_start = DOOR_LEFT_START # Use LEFT door for left wall (2x3: 5,2 to 6,4) for door_dx in range(2): # Door is 2 tiles wide for door_dy in range(3): # Door is 3 tiles tall var x = right_door_start_x + door_dx var y = door_start_y + door_dy if x >= 0 and x < map_size.x and y >= 0 and y < map_size.y: grid[x][y] = 2 # Door area (replaces wall) # Use door tile coordinates (5,2) + offset for 2x3 door tile_grid[x][y] = right_door_tile_start + Vector2i(door_dx, door_dy) if door_dx == 1 and door_dy == 1: grid[x][y] = 1 # Floor var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()] tile_grid[x][y] = floor_tile # CRITICAL: room1 = room the door is ON (left room for horizontal doors) # room2 = room the door leads TO (right room for horizontal doors) # For a door on the RIGHT wall of the left room, room1 = left_room, room2 = right_room var door_room1 = left_room # Door is ON the left room's right wall var door_room2 = right_room # Door leads TO the right room return { "x": door_start_x, "y": door_y, "w": door_width, "h": 1, "dir": "E", "room1": door_room1, # CRITICAL: Door is IN the left room (on its right wall) "room2": door_room2 # Door leads TO the right room } else: # Vertical corridor var top_room = room1 if room1.y < room2.y else room2 var bottom_room = room2 if room1.y < room2.y else room1 if bottom_room.y - (top_room.y + top_room.h) > 20: return {} # Door position - IN THE MIDDLE OF THE WALL (replacing part of the wall) # For vertical doors (up/down), door is 3x2 tiles (3 wide, 2 tall) # The wall is 2 tiles tall (at top_room.y + top_room.h - 2 and top_room.y + top_room.h - 1) # Door must be at least 1 tile away from corners (corners are 2x2, so start at x+2) var wall_y = top_room.y + top_room.h - 1 # Bottom wall of top room (wall is at room.y + room.h - 1) var min_x = max(top_room.x + 2, bottom_room.x + 2) # At least 1 tile from left corner (corner is 2 tiles) var max_x = min(top_room.x + top_room.w - 5, bottom_room.x + bottom_room.w - 5) # At least 1 tile from right corner, and room for 3-tile wide door if max_x < min_x: return {} var door_x = min_x + rng.randi_range(0, max(0, max_x - min_x)) var door_height = rng.randi_range(DOOR_MIN_WIDTH, DOOR_MAX_WIDTH) var corridor_length = bottom_room.y - (top_room.y + top_room.h) # Check if corridor path would intersect any other rooms var corridor_start_y = wall_y + 1 var corridor_end_y = wall_y + corridor_length var corridor_x = door_x + 1 if corridor_intersects_other_room.call(corridor_x, corridor_start_y, corridor_x, corridor_end_y, false): return {} # Corridor would pass through another room, skip this connection # Create corridor (1 tile wide) - use floor tiles # Corridor is between the rooms, after the door for y in range(wall_y + 1, wall_y + corridor_length + 1): # Corridor starts after the wall if door_x + 1 >= 0 and door_x + 1 < map_size.x and y >= 0 and y < map_size.y: grid[door_x + 1][y] = 3 # Corridor (middle column of door) var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()] tile_grid[door_x + 1][y] = floor_tile # Create door on BOTTOM wall of top room (3x2 tiles - 3 wide, 2 tall) # Door is placed ON the wall, replacing the 2-tile tall wall # The wall is at wall_y (top_room.y + top_room.h - 1) and wall_y - 1 (top_room.y + top_room.h - 2) # This is the BOTTOM wall of the top room, so use DOOR_BOTTOM_START (7,5 to 9,6) var door_start_x = door_x # Start from door_x position var door_start_y = wall_y - 1 # Door starts at the upper wall tile (2 tiles tall) var door_tile_start = DOOR_BOTTOM_START # Use BOTTOM door for bottom wall (3x2: 7,5 to 9,6) for door_dx in range(3): # Door is 3 tiles wide for door_dy in range(2): # Door is 2 tiles tall var x = door_start_x + door_dx var y = door_start_y + door_dy if x >= 0 and x < map_size.x and y >= 0 and y < map_size.y: grid[x][y] = 2 # Door area (replaces wall) # Use door tile coordinates (7,5) + offset for 3x2 door tile_grid[x][y] = door_tile_start + Vector2i(door_dx, door_dy) if door_dx == 1 and door_dy == 0: grid[x][y] = 1 # Floor var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()] tile_grid[x][y] = floor_tile # Also create door on TOP wall of bottom room (if there's a gap) if corridor_length > 0: var bottom_wall_y = bottom_room.y # Top wall of bottom room var bottom_door_start_y = bottom_wall_y # Door starts at the top wall tile (2 tiles tall) var bottom_door_tile_start = DOOR_UP_START # Use UP door for top wall (3x2: 7,0 to 9,1) for door_dx in range(3): # Door is 3 tiles wide for door_dy in range(2): # Door is 2 tiles tall var x = door_start_x + door_dx var y = bottom_door_start_y + door_dy if x >= 0 and x < map_size.x and y >= 0 and y < map_size.y: grid[x][y] = 2 # Door area (replaces wall) # Use door tile coordinates (7,0) + offset for 3x2 door tile_grid[x][y] = bottom_door_tile_start + Vector2i(door_dx, door_dy) if door_dx == 1 and door_dy == 1: grid[x][y] = 1 # Floor var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()] tile_grid[x][y] = floor_tile # CRITICAL: room1 = room the door is ON (top room for vertical doors) # room2 = room the door leads TO (bottom room for vertical doors) # For a door on the BOTTOM wall of the top room, room1 = top_room, room2 = bottom_room var door_room1 = top_room # Door is ON the top room's bottom wall var door_room2 = bottom_room # Door leads TO the bottom room return { "x": door_x, "y": door_start_y, "w": 1, "h": door_height, "dir": "S", "room1": door_room1, # CRITICAL: Door is IN the top room (on its bottom wall) "room2": door_room2 # Door leads TO the bottom room } func _find_closest_rooms(room: Dictionary, all_rooms: Array) -> Array: if all_rooms.size() <= 1: return [] var distances = [] for other in all_rooms: if other == room: continue var dist = abs(room.x - other.x) + abs(room.y - other.y) distances.append({"room": other, "distance": dist}) if distances.size() > 0: distances.sort_custom(func(a, b): return a.distance < b.distance) return distances.map(func(item): return item.room) return [] func _rooms_are_connected(room1: Dictionary, room2: Dictionary, doors: Array) -> bool: for door in doors: if (door.room1 == room1 and door.room2 == room2) or \ (door.room1 == room2 and door.room2 == room1): return true return false func _place_torches_in_room(room: Dictionary, grid: Array, all_doors: Array, _map_size: Vector2i, rng: RandomNumberGenerator) -> Array: # Place 0 to 4 torches randomly on walls of the room # Torches cannot be placed exactly on doors var torches = [] var num_torches = rng.randi_range(0, 4) # Collect valid wall positions (excluding doors) var valid_wall_positions = [] var tile_size = 16 # Calculate the torch Y position relative to floor (8 pixels down from top wall center) # This ensures all torches have the same distance to the floor var torch_y_offset = 8 # Pixels down from top wall center var torch_y_from_floor = room.y * tile_size + tile_size / 2.0 + torch_y_offset # Top wall (y = room.y, x from room.x+2 to room.x+room.w-2, excluding corners) for x in range(room.x + 2, room.x + room.w - 2): if _is_valid_torch_position(x, room.y, grid, all_doors): var world_pos = Vector2(x * tile_size + tile_size / 2.0, torch_y_from_floor) valid_wall_positions.append({"pos": world_pos, "wall": "top", "rotation": 0}) # Bottom wall (y = room.y + room.h - 1, x from room.x+2 to room.x+room.w-2) # Place torches at the same distance from floor as top wall torches # Bottom wall center is at: (room.y + room.h - 1) * tile_size + tile_size / 2.0 # To get same distance from floor as top wall: room.y * tile_size + tile_size / 2.0 + 8 # But we need to place it on the bottom wall, so use bottom wall's Y position # Calculate: bottom_wall_y_world - (room.h * tile_size) + torch_y_offset # This gives us the same distance from floor # Bottom wall torches should be 8 pixels up from bottom wall center (closer to floor) # Bottom wall center is at: (room.y + room.h - 1) * tile_size + tile_size / 2.0 # To get same distance from floor as top wall: move up 8 pixels from bottom wall center var bottom_wall_center_y = (room.y + room.h - 1) * tile_size + tile_size / 2.0 var bottom_torch_y = bottom_wall_center_y - torch_y_offset # Move up 8 pixels from bottom wall center for x in range(room.x + 2, room.x + room.w - 2): # Check if this is a valid bottom wall position (check the lower part of bottom wall) var bottom_wall_y = room.y + room.h - 1 if _is_valid_torch_position(x, bottom_wall_y, grid, all_doors): # Place at bottom wall Y position, but 8 pixels up from center (closer to floor) var world_pos = Vector2(x * tile_size + tile_size / 2.0, bottom_torch_y) valid_wall_positions.append({"pos": world_pos, "wall": "bottom", "rotation": 180}) # Left wall (2 tiles wide: room.x and room.x + 1) # Place torches at the same Y position as top wall torches (same distance from floor) # torch_y_from_floor = room.y * tile_size + tile_size / 2.0 + 8 # Left wall has two parts: room.x (left part, also corner) and room.x + 1 (right part, actual wall) # We should place torches on room.x + 1 (the right part of the 2-tile wide left wall), not on room.x (corner) # Find which Y tile position matches torch_y_from_floor # torch_y_from_floor = room.y * tile_size + 8 + 8 = room.y * tile_size + 16 # target_y_tile = int((room.y * 16 + 16) / 16) = room.y + 1 # But we need to check if room.y + 1 is in the valid range (room.y + 2 to room.y + room.h - 2) # Actually, torch_y_from_floor is 8 pixels down from top wall center, which is still in the top wall tile # So target_y_tile = room.y + 1, but we need to check if it's >= room.y + 2 (excluding corners) # Since target_y_tile = room.y + 1, and we check >= room.y + 2, it will always fail! # We need to check all valid Y positions on the left/right walls and place at torch_y_from_floor # Left/right walls are valid from room.y + 2 to room.y + room.h - 2 (skipping 2-tile corners) for y in range(room.y + 2, room.y + room.h - 2): # First check if there's a door at this Y position on the left wall # Left door (dir="W") is 3 tiles tall, so check if y is in any door's Y range var has_door_at_y = false for door in all_doors: var door_dir = door.dir if "dir" in door else "" if door_dir == "W" and door.x == room.x: # Left door at room.x, check if y is within door's Y range (door.y to door.y + 3) if y >= door.y and y <= door.y + 3: has_door_at_y = true break if has_door_at_y: continue # Skip this Y position if there's a door here # Check if this is a valid left wall position # Left wall has 2 tiles: room.x and room.x + 1 # Check both tiles to ensure we're not placing on a door # Use room.x + 1 (the right part of the left wall) for torch placement if _is_valid_torch_position(room.x, y, grid, all_doors) and _is_valid_torch_position(room.x + 1, y, grid, all_doors): # Calculate torch world position # X position is on the left wall: (room.x + 1) * tile_size + tile_size / 2.0 # Move it further to the left (negative X) to position it better on the wall var left_wall_x = (room.x + 1) * tile_size + tile_size / 2.0 - 8 # Move 8 pixels to the left # Y position should be at the same distance from floor: y * tile_size + tile_size / 2.0 + 8 var left_wall_y = y * tile_size + tile_size / 2.0 + torch_y_offset var world_pos = Vector2(left_wall_x, left_wall_y) # CRITICAL: Check if torch's 16x16 pixel bounding box overlaps with any door # Torch is 16x16 pixels, so it extends 8 pixels in each direction from its center # Torch bounding box: from (left_wall_x - 8, left_wall_y - 8) to (left_wall_x + 8, left_wall_y + 8) var torch_bbox_min_x = left_wall_x - 8 var torch_bbox_max_x = left_wall_x + 8 var torch_bbox_min_y = left_wall_y - 8 var torch_bbox_max_y = left_wall_y + 8 # Check if torch bounding box overlaps with any door's bounding box # Only check doors that are on the left wall of this room (dir="W") var overlaps_door = false var tile_size_check = 16 for door in all_doors: var door_dir = door.dir if "dir" in door else "" if door_dir != "W": # Only check left doors (dir="W") continue # Check if this door is on the left wall of this room # Left door is on the left wall, so door.x should be room.x (the left tile of the left wall) var door_x_match = (door.x == room.x) if not door_x_match: continue # This door is not on this room's left wall # Left door (dir="W"): 2 tiles wide, 3 tiles tall # Door position is at door.x, door.y (upper-left tile) # Door occupies tiles: x from door.x to door.x + 2, y from door.y to door.y + 3 # Door world bounding box: from (door.x * 16, door.y * 16) to ((door.x + 2) * 16, (door.y + 3) * 16) # CRITICAL: A torch is 16x16 pixels (8px in each direction), so expand door bounds by 8px in ALL directions # Door occupies columns room.x (0) and room.x + 1 (1), so torch at room.x + 1 can overlap if it extends left var door_min_x = door.x * tile_size_check - 8 var door_max_x = (door.x + 2) * tile_size_check + 8 var door_min_y = door.y * tile_size_check - 8 var door_max_y = (door.y + 3) * tile_size_check + 8 # Check if torch bounding box overlaps with door bounding box (non-overlapping means torch is safe) # Overlap exists if NOT (torch_max < door_min OR torch_min > door_max) var x_overlap = not (torch_bbox_max_x < door_min_x or torch_bbox_min_x > door_max_x) var y_overlap = not (torch_bbox_max_y < door_min_y or torch_bbox_min_y > door_max_y) if x_overlap and y_overlap: overlaps_door = true break if not overlaps_door: valid_wall_positions.append({"pos": world_pos, "wall": "left", "rotation": 270}) break # Only add one torch per wall # Right wall (2 tiles wide: room.x + room.w - 2 and room.x + room.w - 1) # Place torches at the same distance from floor as top wall torches # Right wall has two parts: room.x + room.w - 2 (left part, actual wall) and room.x + room.w - 1 (right part, also corner) # We should place torches on room.x + room.w - 2 (the left part of the 2-tile wide right wall), not on room.x + room.w - 1 (corner) # Check all valid Y positions on the right wall # CRITICAL: Check both tiles of the right wall (similar to left wall) to ensure we're not placing on a door for y in range(room.y + 2, room.y + room.h - 2): # Check if this is a valid right wall position # Right wall has 2 tiles: room.x + room.w - 2 (left part) and room.x + room.w - 1 (right part/corner) # Check both tiles to ensure we're not placing on a door # Use room.x + room.w - 2 (the left part of the right wall) for torch placement if _is_valid_torch_position(room.x + room.w - 2, y, grid, all_doors) and _is_valid_torch_position(room.x + room.w - 1, y, grid, all_doors): # Calculate torch world position # X position is on the right wall: (room.x + room.w - 2) * tile_size + tile_size / 2.0 # Move it further to the right (positive X) to position it better on the wall var right_wall_x = (room.x + room.w - 2) * tile_size + tile_size / 2.0 + 8 # Move 8 pixels to the right # Y position should be at the same distance from floor: y * tile_size + tile_size / 2.0 + 8 var right_wall_y = y * tile_size + tile_size / 2.0 + torch_y_offset var world_pos = Vector2(right_wall_x, right_wall_y) # CRITICAL: Check if torch's 16x16 pixel bounding box overlaps with any door # Torch is 16x16 pixels, so it extends 8 pixels in each direction from its center # Torch bounding box: from (right_wall_x - 8, right_wall_y - 8) to (right_wall_x + 8, right_wall_y + 8) var torch_bbox_min_x = right_wall_x - 8 var torch_bbox_max_x = right_wall_x + 8 var torch_bbox_min_y = right_wall_y - 8 var torch_bbox_max_y = right_wall_y + 8 # Check if torch bounding box overlaps with any door's bounding box # Only check doors that are on the right wall of this room (dir="E") var overlaps_door = false var tile_size_check = 16 for door in all_doors: var door_dir = door.dir if "dir" in door else "" if door_dir != "E": # Only check right doors (dir="E") continue # Check if this door is on the right wall of this room # Right door is on the right wall, so door.x should be room.x + room.w - 2 (the left part of the right wall) var door_x_match = (door.x == room.x + room.w - 2) if not door_x_match: continue # This door is not on this room's right wall # Right door (dir="E"): 2 tiles wide, 3 tiles tall # Door position is at door.x, door.y (upper-left tile) # Door occupies tiles: x from door.x to door.x + 2, y from door.y to door.y + 3 # Door world bounding box: from (door.x * 16, door.y * 16) to ((door.x + 2) * 16, (door.y + 3) * 16) var door_min_x = door.x * tile_size_check var door_max_x = (door.x + 2) * tile_size_check var door_min_y = door.y * tile_size_check var door_max_y = (door.y + 3) * tile_size_check # Check if torch bounding box overlaps with door bounding box if not (torch_bbox_max_x < door_min_x or torch_bbox_min_x > door_max_x or \ torch_bbox_max_y < door_min_y or torch_bbox_min_y > door_max_y): overlaps_door = true break if not overlaps_door: valid_wall_positions.append({"pos": world_pos, "wall": "right", "rotation": 90}) break # Only add one torch per wall # Randomly select torch positions if valid_wall_positions.size() == 0: return torches valid_wall_positions.shuffle() for i in range(min(num_torches, valid_wall_positions.size())): var torch_data = valid_wall_positions[i] torches.append({ "position": torch_data.pos, "rotation": torch_data.rotation }) return torches func _is_valid_torch_position(x: int, y: int, grid: Array, all_doors: Array) -> bool: # Check if position is within bounds if x < 0 or y < 0: return false if x >= grid.size() or y >= grid[x].size(): return false # Check grid value - torches can only be placed on wall tiles (grid_value == 0) # Cannot place on: doors (2), corridors (3), stairs (4), or floor (1) var grid_value = grid[x][y] if grid_value != 0: # Only wall tiles (0) are valid for torches # Specifically check for stairs (4) to prevent overlap if grid_value == 4: return false # Stairs tile if grid_value == 2: return false # Door tile # Any other non-wall value is invalid return false # Also check if position is within door area from door dictionaries # This is a backup check in case door grid values aren't set yet for door in all_doors: var door_x = door.x var door_y = door.y var door_dir = door.dir if "dir" in door else "" # Calculate actual door dimensions based on direction # Horizontal doors (E/W): actually 2-3 tiles wide and 3 tiles tall in grid # Vertical doors (N/S): actually 3 tiles wide and 2-3 tiles tall in grid var door_w = door.w if "w" in door else 2 var door_h = door.h if "h" in door else 3 var actual_w = door_w var actual_h = door_h if door_dir == "E" or door_dir == "W": # Horizontal door: w is correct (2 or 3), but h is actually 3 in grid (not 1) actual_h = 3 elif door_dir == "N" or door_dir == "S": # Vertical door: h is correct (2 or 3), but w is actually 3 in grid (not 1) actual_w = 3 # Check if (x, y) is within door area if x >= door_x and x < door_x + actual_w and y >= door_y and y < door_y + actual_h: return false return true func _find_reachable_rooms(start_room: Dictionary, _all_rooms: Array, all_doors: Array) -> Array: var reachable = [start_room] var queue = [start_room] # Helper function to check if a room is already in the reachable list (value-based comparison) var is_room_in_list = func(room_list: Array, room: Dictionary) -> bool: if not room or room.is_empty(): return false for r in room_list: if r.x == room.x and r.y == room.y and r.w == room.w and r.h == room.h: return true return false while queue.size() > 0: var current = queue.pop_front() for door in all_doors: var next_room = null # Use value-based comparison for room matching if "room1" in door and door.room1 is Dictionary: if door.room1.x == current.x and door.room1.y == current.y and \ door.room1.w == current.w and door.room1.h == current.h: next_room = door.room2 if "room2" in door else null if next_room == null and "room2" in door and door.room2 is Dictionary: if door.room2.x == current.x and door.room2.y == current.y and \ door.room2.w == current.w and door.room2.h == current.h: next_room = door.room1 if "room1" in door else null if next_room != null and next_room is Dictionary and not next_room.is_empty(): if not is_room_in_list.call(reachable, next_room): reachable.append(next_room) queue.append(next_room) return reachable func _add_hole_to_room(room: Dictionary, grid: Array, tile_grid: Array, map_size: Vector2i, rng: RandomNumberGenerator): # Add a hole (minimum 9x9 tiles) somewhere in the room # Holes should only be placed in the floor area (not in walls) # Floor area is from room.x + 2 to room.x + room.w - 2, and room.y + 2 to room.y + room.h - 2 var floor_min_x = room.x + 2 var floor_min_y = room.y + 2 var floor_max_x = room.x + room.w - 2 var floor_max_y = room.y + room.h - 2 var floor_w = floor_max_x - floor_min_x var floor_h = floor_max_y - floor_min_y if floor_w < MIN_HOLE_SIZE or floor_h < MIN_HOLE_SIZE: return # Room too small for hole var hole_size = rng.randi_range(MIN_HOLE_SIZE, min(floor_w, floor_h, 12)) # Position hole within floor area (with 1 tile margin from floor edges) var max_x = floor_max_x - hole_size var max_y = floor_max_y - hole_size if max_x < floor_min_x or max_y < floor_min_y: return # Floor area too small for hole var hole_x = rng.randi_range(floor_min_x, max_x) var hole_y = rng.randi_range(floor_min_y, max_y) # Create hole (back to wall) - use inner wall tiles # Only create hole if the position is currently a floor tile for x in range(hole_x, hole_x + hole_size): for y in range(hole_y, hole_y + hole_size): if x >= 0 and x < map_size.x and y >= 0 and y < map_size.y: # Only create hole if it's currently a floor tile if grid[x][y] == 1: # Floor grid[x][y] = 0 # Wall # Use inner wall tiles for holes if x == hole_x and y == hole_y: tile_grid[x][y] = INNER_WALL_TOP_LEFT elif x == hole_x + hole_size - 1 and y == hole_y: tile_grid[x][y] = INNER_WALL_TOP_RIGHT elif x == hole_x and y == hole_y + hole_size - 1: tile_grid[x][y] = INNER_WALL_BOTTOM_LEFT elif x == hole_x + hole_size - 1 and y == hole_y + hole_size - 1: tile_grid[x][y] = INNER_WALL_BOTTOM_RIGHT else: # Use default wall tile for interior of hole tile_grid[x][y] = WALL_TOP_UPPER func _find_farthest_room(all_rooms: Array, start_index: int) -> int: var start_room = all_rooms[start_index] var max_distance = 0 var farthest_index = 0 for i in range(all_rooms.size()): if i == start_index: continue var room = all_rooms[i] var distance = abs(room.x - start_room.x) + abs(room.y - start_room.y) if distance > max_distance: max_distance = distance farthest_index = i return farthest_index func _find_farthest_room_from_list(all_rooms: Array, start_index: int, reachable_rooms: Array) -> int: # Find the farthest room from the start room, but only from the reachable rooms list var start_room = all_rooms[start_index] var max_distance = 0 var farthest_index = -1 for i in range(all_rooms.size()): if i == start_index: continue var room = all_rooms[i] # Only consider reachable rooms if not reachable_rooms.has(room): continue var distance = abs(room.x - start_room.x) + abs(room.y - start_room.y) if distance > max_distance: max_distance = distance farthest_index = i return farthest_index func _render_room_walls(all_rooms: Array, grid: Array, tile_grid: Array, map_size: Vector2i, _rng: RandomNumberGenerator): # Render walls around each room # All walls are 2 tiles tall (upper and lower parts) # Walls are placed at room boundaries (room.x, room.y, etc.) # IMPORTANT: Only render walls where grid[x][y] == 0 (wall) or grid[x][y] == 2 (door) # Do NOT overwrite floor tiles (grid[x][y] == 1) or corridors (grid[x][y] == 3) for room in all_rooms: # Top-left corner (2x2): (room.x, room.y), (room.x+1, room.y), (room.x, room.y+1), (room.x+1, room.y+1) # Only render if it's a wall (0) or door (2), not floor (1) or corridor (3) if room.x >= 0 and room.x < map_size.x and room.y >= 0 and room.y < map_size.y: if grid[room.x][room.y] == 0 or grid[room.x][room.y] == 2: # Wall or door if grid[room.x][room.y] != 2: # Not a door (don't overwrite door tiles) tile_grid[room.x][room.y] = WALL_TOP_LEFT_TOP_LEFT if room.x + 1 >= 0 and room.x + 1 < map_size.x and room.y >= 0 and room.y < map_size.y: if grid[room.x + 1][room.y] == 0 or grid[room.x + 1][room.y] == 2: # Wall or door if grid[room.x + 1][room.y] != 2: # Not a door tile_grid[room.x + 1][room.y] = WALL_TOP_LEFT_TOP_RIGHT if room.x >= 0 and room.x < map_size.x and room.y + 1 >= 0 and room.y + 1 < map_size.y: if grid[room.x][room.y + 1] == 0 or grid[room.x][room.y + 1] == 2: # Wall or door if grid[room.x][room.y + 1] != 2: # Not a door tile_grid[room.x][room.y + 1] = WALL_TOP_LEFT_BOTTOM_LEFT if room.x + 1 >= 0 and room.x + 1 < map_size.x and room.y + 1 >= 0 and room.y + 1 < map_size.y: if grid[room.x + 1][room.y + 1] == 0 or grid[room.x + 1][room.y + 1] == 2: # Wall or door if grid[room.x + 1][room.y + 1] != 2: # Not a door tile_grid[room.x + 1][room.y + 1] = WALL_TOP_LEFT_BOTTOM_RIGHT # Top-right corner (2x2): (room.x+room.w-2, room.y), (room.x+room.w-1, room.y), (room.x+room.w-2, room.y+1), (room.x+room.w-1, room.y+1) # Only render if it's a wall (0) or door (2), not floor (1) or corridor (3) var top_right_x_left = room.x + room.w - 2 var top_right_x_right = room.x + room.w - 1 if top_right_x_left >= 0 and top_right_x_left < map_size.x and room.y >= 0 and room.y < map_size.y: if grid[top_right_x_left][room.y] == 0 or grid[top_right_x_left][room.y] == 2: # Wall or door if grid[top_right_x_left][room.y] != 2: # Not a door (don't overwrite door tiles) tile_grid[top_right_x_left][room.y] = WALL_TOP_RIGHT_TOP_LEFT if top_right_x_right >= 0 and top_right_x_right < map_size.x and room.y >= 0 and room.y < map_size.y: if grid[top_right_x_right][room.y] == 0 or grid[top_right_x_right][room.y] == 2: # Wall or door if grid[top_right_x_right][room.y] != 2: # Not a door tile_grid[top_right_x_right][room.y] = WALL_TOP_RIGHT_TOP_RIGHT if top_right_x_left >= 0 and top_right_x_left < map_size.x and room.y + 1 >= 0 and room.y + 1 < map_size.y: if grid[top_right_x_left][room.y + 1] == 0 or grid[top_right_x_left][room.y + 1] == 2: # Wall or door if grid[top_right_x_left][room.y + 1] != 2: # Not a door tile_grid[top_right_x_left][room.y + 1] = WALL_TOP_RIGHT_BOTTOM_LEFT if top_right_x_right >= 0 and top_right_x_right < map_size.x and room.y + 1 >= 0 and room.y + 1 < map_size.y: if grid[top_right_x_right][room.y + 1] == 0 or grid[top_right_x_right][room.y + 1] == 2: # Wall or door if grid[top_right_x_right][room.y + 1] != 2: # Not a door tile_grid[top_right_x_right][room.y + 1] = WALL_TOP_RIGHT_BOTTOM_RIGHT # Top wall (2 tiles tall) - between corners for x in range(room.x + 2, room.x + room.w - 2): # Upper part of top wall (at room.y) if x >= 0 and x < map_size.x and room.y >= 0 and room.y < map_size.y: # Only render wall if it's a wall (0) or door (2), not floor (1) or corridor (3) if grid[x][room.y] == 0 or grid[x][room.y] == 2: # Wall or door if grid[x][room.y] != 2: # Not a door (don't overwrite door tiles) tile_grid[x][room.y] = WALL_TOP_UPPER # Lower part of top wall (at room.y + 1) if x >= 0 and x < map_size.x and room.y + 1 >= 0 and room.y + 1 < map_size.y: # Only render wall if it's a wall (0) or door (2), not floor (1) or corridor (3) if grid[x][room.y + 1] == 0 or grid[x][room.y + 1] == 2: # Wall or door if grid[x][room.y + 1] != 2: # Not a door (don't overwrite door tiles) tile_grid[x][room.y + 1] = WALL_TOP_LOWER # Bottom-left corner (2x2): (room.x, room.y+room.h-2), (room.x+1, room.y+room.h-2), (room.x, room.y+room.h-1), (room.x+1, room.y+room.h-1) # Only render if it's a wall (0) or door (2), not floor (1) or corridor (3) var bottom_y = room.y + room.h - 1 var bottom_y_upper = room.y + room.h - 2 if room.x >= 0 and room.x < map_size.x and bottom_y_upper >= 0 and bottom_y_upper < map_size.y: if grid[room.x][bottom_y_upper] == 0 or grid[room.x][bottom_y_upper] == 2: # Wall or door if grid[room.x][bottom_y_upper] != 2: # Not a door (don't overwrite door tiles) tile_grid[room.x][bottom_y_upper] = WALL_BOTTOM_LEFT_TOP_LEFT if room.x + 1 >= 0 and room.x + 1 < map_size.x and bottom_y_upper >= 0 and bottom_y_upper < map_size.y: if grid[room.x + 1][bottom_y_upper] == 0 or grid[room.x + 1][bottom_y_upper] == 2: # Wall or door if grid[room.x + 1][bottom_y_upper] != 2: # Not a door tile_grid[room.x + 1][bottom_y_upper] = WALL_BOTTOM_LEFT_TOP_RIGHT if room.x >= 0 and room.x < map_size.x and bottom_y >= 0 and bottom_y < map_size.y: if grid[room.x][bottom_y] == 0 or grid[room.x][bottom_y] == 2: # Wall or door if grid[room.x][bottom_y] != 2: # Not a door tile_grid[room.x][bottom_y] = WALL_BOTTOM_LEFT_BOTTOM_LEFT if room.x + 1 >= 0 and room.x + 1 < map_size.x and bottom_y >= 0 and bottom_y < map_size.y: if grid[room.x + 1][bottom_y] == 0 or grid[room.x + 1][bottom_y] == 2: # Wall or door if grid[room.x + 1][bottom_y] != 2: # Not a door tile_grid[room.x + 1][bottom_y] = WALL_BOTTOM_LEFT_BOTTOM_RIGHT # Bottom-right corner (2x2): (room.x+room.w-2, room.y+room.h-2), (room.x+room.w-1, room.y+room.h-2), (room.x+room.w-2, room.y+room.h-1), (room.x+room.w-1, room.y+room.h-1) # Only render if it's a wall (0) or door (2), not floor (1) or corridor (3) var bottom_right_x_left = room.x + room.w - 2 var bottom_right_x_right = room.x + room.w - 1 if bottom_right_x_left >= 0 and bottom_right_x_left < map_size.x and bottom_y_upper >= 0 and bottom_y_upper < map_size.y: if grid[bottom_right_x_left][bottom_y_upper] == 0 or grid[bottom_right_x_left][bottom_y_upper] == 2: # Wall or door if grid[bottom_right_x_left][bottom_y_upper] != 2: # Not a door (don't overwrite door tiles) tile_grid[bottom_right_x_left][bottom_y_upper] = WALL_BOTTOM_RIGHT_TOP_LEFT if bottom_right_x_right >= 0 and bottom_right_x_right < map_size.x and bottom_y_upper >= 0 and bottom_y_upper < map_size.y: if grid[bottom_right_x_right][bottom_y_upper] == 0 or grid[bottom_right_x_right][bottom_y_upper] == 2: # Wall or door if grid[bottom_right_x_right][bottom_y_upper] != 2: # Not a door tile_grid[bottom_right_x_right][bottom_y_upper] = WALL_BOTTOM_RIGHT_TOP_RIGHT if bottom_right_x_left >= 0 and bottom_right_x_left < map_size.x and bottom_y >= 0 and bottom_y < map_size.y: if grid[bottom_right_x_left][bottom_y] == 0 or grid[bottom_right_x_left][bottom_y] == 2: # Wall or door if grid[bottom_right_x_left][bottom_y] != 2: # Not a door tile_grid[bottom_right_x_left][bottom_y] = WALL_BOTTOM_RIGHT_BOTTOM_LEFT if bottom_right_x_right >= 0 and bottom_right_x_right < map_size.x and bottom_y >= 0 and bottom_y < map_size.y: if grid[bottom_right_x_right][bottom_y] == 0 or grid[bottom_right_x_right][bottom_y] == 2: # Wall or door if grid[bottom_right_x_right][bottom_y] != 2: # Not a door tile_grid[bottom_right_x_right][bottom_y] = WALL_BOTTOM_RIGHT_BOTTOM_RIGHT # Bottom wall (2 tiles tall) - between corners for x in range(room.x + 2, room.x + room.w - 2): # Upper part of bottom wall (at room.y + room.h - 2) if x >= 0 and x < map_size.x and bottom_y_upper >= 0 and bottom_y_upper < map_size.y: # Only render wall if it's a wall (0) or door (2), not floor (1) or corridor (3) if grid[x][bottom_y_upper] == 0 or grid[x][bottom_y_upper] == 2: # Wall or door if grid[x][bottom_y_upper] != 2: # Not a door (don't overwrite door tiles) tile_grid[x][bottom_y_upper] = WALL_BOTTOM_UPPER # Lower part of bottom wall (at room.y + room.h - 1) if x >= 0 and x < map_size.x and bottom_y >= 0 and bottom_y < map_size.y: # Only render wall if it's a wall (0) or door (2), not floor (1) or corridor (3) if grid[x][bottom_y] == 0 or grid[x][bottom_y] == 2: # Wall or door if grid[x][bottom_y] != 2: # Not a door (don't overwrite door tiles) tile_grid[x][bottom_y] = WALL_BOTTOM_LOWER # Left wall (2 tiles wide) - at room.x and room.x + 1 (left and right parts) for y in range(room.y + 2, room.y + room.h - 2): # Skip corners (first 2 and last 2 rows) # Left part of left wall (at room.x) if room.x >= 0 and room.x < map_size.x and y >= 0 and y < map_size.y: # Only render wall if it's a wall (0) or door (2), not floor (1) or corridor (3) if grid[room.x][y] == 0 or grid[room.x][y] == 2: # Wall or door if grid[room.x][y] != 2: # Not a door (don't overwrite door tiles) tile_grid[room.x][y] = WALL_LEFT_LEFT # Right part of left wall (at room.x + 1) if room.x + 1 >= 0 and room.x + 1 < map_size.x and y >= 0 and y < map_size.y: # Only render wall if it's a wall (0) or door (2), not floor (1) or corridor (3) if grid[room.x + 1][y] == 0 or grid[room.x + 1][y] == 2: # Wall or door if grid[room.x + 1][y] != 2: # Not a door (don't overwrite door tiles) tile_grid[room.x + 1][y] = WALL_LEFT_RIGHT # Right wall (2 tiles wide) - at room.x + room.w - 2 and room.x + room.w - 1 (left and right parts) var right_x_left = room.x + room.w - 2 var right_x_right = room.x + room.w - 1 for y in range(room.y + 2, room.y + room.h - 2): # Skip corners (first 2 and last 2 rows) # Left part of right wall (at room.x + room.w - 2) if right_x_left >= 0 and right_x_left < map_size.x and y >= 0 and y < map_size.y: # Only render wall if it's a wall (0) or door (2), not floor (1) or corridor (3) if grid[right_x_left][y] == 0 or grid[right_x_left][y] == 2: # Wall or door if grid[right_x_left][y] != 2: # Not a door (don't overwrite door tiles) tile_grid[right_x_left][y] = WALL_RIGHT_LEFT # Right part of right wall (at room.x + room.w - 1) if right_x_right >= 0 and right_x_right < map_size.x and y >= 0 and y < map_size.y: # Only render wall if it's a wall (0) or door (2), not floor (1) or corridor (3) if grid[right_x_right][y] == 0 or grid[right_x_right][y] == 2: # Wall or door if grid[right_x_right][y] != 2: # Not a door (don't overwrite door tiles) tile_grid[right_x_right][y] = WALL_RIGHT_RIGHT func _place_enemies_in_room(room: Dictionary, grid: Array, map_size: Vector2i, rng: RandomNumberGenerator, level: int = 1) -> Array: # Place enemies in a room, scaled by level # Level 1: 0-2 enemies per room (fewer) # Level 2+: 0-4 enemies per room, increasing max with level # Returns array of enemy data dictionaries var enemies = [] # Calculate max enemies based on level # Level 1: max 2 enemies per room (fewer for first level) # Level 2: max 3 enemies per room # Level 3: max 4 enemies per room # Level 4+: max 5-6 enemies per room (scales with level) var max_enemies = 2 if level == 1 else min(1 + level, 6) # Level 1: 2, Level 2: 3, Level 3: 4, Level 4: 5, Level 5+: 6 var num_enemies = rng.randi_range(0, max_enemies) # Available enemy types (scene paths) var enemy_types = [ "res://scenes/enemy_rat.tscn", "res://scenes/enemy_humanoid.tscn", "res://scenes/enemy_slime.tscn", "res://scenes/enemy_bat.tscn" ] # Find valid floor positions in the room (excluding walls) var valid_positions = [] var tile_size = 16 # 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 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: return enemies # Shuffle positions to randomize enemy placement valid_positions.shuffle() # Place enemies for i in range(min(num_enemies, valid_positions.size())): var enemy_type = enemy_types[rng.randi() % enemy_types.size()] var position = valid_positions[i] # Randomize stats (similar to player but weaker) # Base stats vary by enemy type var max_health = rng.randf_range(18.0, 35.0) # Reduced from 30.0-60.0 for better balance var move_speed: float var damage = rng.randf_range(5.0, 15.0) # Set appropriate move speed based on enemy type if enemy_type.ends_with("bat.tscn"): move_speed = rng.randf_range(35.0, 45.0) # Bats: slower elif enemy_type.ends_with("slime.tscn"): move_speed = rng.randf_range(18.0, 25.0) # Slimes: very slow (reduced from 30-40) elif enemy_type.ends_with("rat.tscn"): move_speed = rng.randf_range(40.0, 50.0) # Rats: slow else: move_speed = rng.randf_range(50.0, 80.0) # Other enemies (humanoids): faster var enemy_data = { "type": enemy_type, "position": position, "room": room, # Store reference to room for AI "max_health": max_health, "move_speed": move_speed, "damage": damage } # If it's a humanoid enemy, randomize the humanoid_type if enemy_type.ends_with("enemy_humanoid.tscn"): # Random humanoid type: 0=CYCLOPS, 1=DEMON, 2=HUMANOID, 3=NIGHTELF, 4=GOBLIN, 5=ORC, 6=SKELETON # Weight towards common types (goblins, humans, orcs) - 40% goblin, 30% humanoid, 20% orc, 10% other var rand_val = rng.randf() var humanoid_type = 2 # Default to HUMANOID if rand_val < 0.4: humanoid_type = 4 # GOBLIN (40%) elif rand_val < 0.7: humanoid_type = 2 # HUMANOID (30%) elif rand_val < 0.9: humanoid_type = 5 # ORC (20%) else: # 10% for other types (distributed evenly) var other_types = [0, 1, 3, 6] # CYCLOPS, DEMON, NIGHTELF, SKELETON humanoid_type = other_types[rng.randi() % other_types.size()] enemy_data["humanoid_type"] = humanoid_type enemies.append(enemy_data) return enemies func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: Array, map_size: Vector2i, all_doors: Array, rng: RandomNumberGenerator) -> Dictionary: # Place stairs in the exit room on one of the walls # Stairs are rendered like doors but with different middle frame tile # Choose a random wall to place stairs on (excluding corners) # Make sure stairs don't overlap any doors # Returns stairs data with position and size for Area2D creation LogManager.log("DungeonGenerator: Placing stairs in exit room: " + str(exit_room.x) + "," + str(exit_room.y) + " size: " + str(exit_room.w) + "x" + str(exit_room.h) + " doors: " + str(all_doors.size()), LogManager.CATEGORY_DUNGEON) var stairs_data: Dictionary = {} var wall_choices = [] var tile_size = 16 # Helper function to check if stairs position overlaps with any door # This checks if ANY tile of the stairs overlaps ANY tile of any door # Doors are either 3x2 (vertical: N/S) or 2x3 (horizontal: E/W) # CRITICAL: Must check ALL door tiles, not just door position! # Uses both door dictionary AND grid check for reliability var stairs_overlaps_door = func(stairs_x: int, stairs_y: int, stairs_w: int, stairs_h: int) -> bool: # FIRST: Check grid - if any stairs tile is marked as a door (grid value 2), it's an overlap for stairs_tile_x in range(stairs_x, stairs_x + stairs_w): for stairs_tile_y in range(stairs_y, stairs_y + stairs_h): if stairs_tile_x >= 0 and stairs_tile_x < map_size.x and stairs_tile_y >= 0 and stairs_tile_y < map_size.y: if grid[stairs_tile_x][stairs_tile_y] == 2: # Grid value 2 = door LogManager.log("DungeonGenerator: Stairs tile (" + str(stairs_tile_x) + "," + str(stairs_tile_y) + ") is marked as door in grid!", LogManager.CATEGORY_DUNGEON) return true # SECOND: Check door dictionary - verify against all known doors for stairs_tile_x in range(stairs_x, stairs_x + stairs_w): for stairs_tile_y in range(stairs_y, stairs_y + stairs_h): # Check this stairs tile against all door tiles from door dictionary for door in all_doors: var door_x = door.x var door_y = door.y # Determine actual door dimensions and tile positions based on direction # CRITICAL: Door x,y is the START position, but door spans multiple tiles! var door_tiles: Array = [] # Array of {x, y} for each door tile if "dir" in door: match door.dir: "E", "W": # Horizontal doors (E/W): 2x3 tiles (2 wide, 3 tall) # Door starts at (door_x, door_y) and spans 2x3 for door_dx in range(2): for door_dy in range(3): door_tiles.append({"x": door_x + door_dx, "y": door_y + door_dy}) "N", "S": # Vertical doors (N/S): 3x2 tiles (3 wide, 2 tall) # Door starts at (door_x, door_y) and spans 3x2 for door_dx in range(3): for door_dy in range(2): door_tiles.append({"x": door_x + door_dx, "y": door_y + door_dy}) _: # Fallback: assume 3x2 (vertical) for door_dx in range(3): for door_dy in range(2): door_tiles.append({"x": door_x + door_dx, "y": door_y + door_dy}) else: # Fallback if no direction: assume 3x2 (vertical) for door_dx in range(3): for door_dy in range(2): door_tiles.append({"x": door_x + door_dx, "y": door_y + door_dy}) # Check if this stairs tile matches any door tile for door_tile in door_tiles: if stairs_tile_x == door_tile.x and stairs_tile_y == door_tile.y: var door_dir_str = str(door.dir) if "dir" in door else "unknown" LogManager.log("DungeonGenerator: Stairs tile (" + str(stairs_tile_x) + "," + str(stairs_tile_y) + ") overlaps door tile (" + str(door_tile.x) + "," + str(door_tile.y) + ") from door at (" + str(door_x) + "," + str(door_y) + ") dir: " + door_dir_str, LogManager.CATEGORY_DUNGEON) return true return false # Determine which walls are available (not blocked by doors) # Use same logic as doors: at least 2 tiles from corners, 5 tiles from opposite edge # If room is exactly 3 tiles wide/tall, stairs must be exactly in the middle # Top wall - stairs are 3 tiles wide, need at least 2 tiles from corners (same as doors) # Minimum room width: 2 (left buffer) + 3 (stairs) + 2 (right buffer) = 7 tiles if exit_room.w >= 7: # Minimum room width for 3-tile stairs with corner buffers var min_x = exit_room.x + 2 # At least 2 tiles from left corner var max_x = exit_room.x + exit_room.w - 5 # At least 5 tiles from right edge (2 buffer + 3 stairs) if max_x >= min_x: wall_choices.append({ "dir": "UP", "x_range": range(min_x, max_x + 1), # +1 because range is exclusive "y": exit_room.y, "tile_start": STAIRS_UP_START, "w": 3, "h": 2 }) # Bottom wall - stairs are 3 tiles wide, need at least 2 tiles from corners (same as doors) # Minimum room width: 2 (left buffer) + 3 (stairs) + 2 (right buffer) = 7 tiles # Use same logic as doors: at least 2 tiles from left corner, 5 tiles from right edge if exit_room.w >= 7: var min_x = exit_room.x + 2 # At least 2 tiles from left corner (same as doors) var max_x = exit_room.x + exit_room.w - 5 # At least 5 tiles from right edge (2 buffer + 3 stairs) if max_x >= min_x: # Bottom wall: door is at exit_room.y + exit_room.h - 2 (upper tile) and exit_room.y + exit_room.h - 1 (lower tile) # Stairs should start at the same position as doors wall_choices.append({ "dir": "DOWN", "x_range": range(min_x, max_x + 1), "y": exit_room.y + exit_room.h - 2, # Start at upper wall tile (same as doors) "tile_start": STAIRS_DOWN_START, "w": 3, "h": 2 }) # Left wall - stairs are 3 tiles tall, need at least 2 tiles from corners # Minimum room height: 2 (top buffer) + 3 (stairs) + 2 (bottom buffer) = 7 tiles if exit_room.h >= 7: # Minimum room height for 3-tile stairs with corner buffers var min_y = exit_room.y + 2 # At least 2 tiles from top corner var max_y = exit_room.y + exit_room.h - 5 # At least 5 tiles from bottom edge (2 buffer + 3 stairs) if max_y >= min_y: wall_choices.append({ "dir": "LEFT", "y_range": range(min_y, max_y + 1), "x": exit_room.x, "tile_start": STAIRS_LEFT_START, "w": 2, "h": 3 }) # Right wall - stairs are 3 tiles tall, need at least 2 tiles from corners # Minimum room height: 2 (top buffer) + 3 (stairs) + 2 (bottom buffer) = 7 tiles if exit_room.h >= 7: var min_y = exit_room.y + 2 # At least 2 tiles from top corner var max_y = exit_room.y + exit_room.h - 5 # At least 5 tiles from bottom edge (2 buffer + 3 stairs) if max_y >= min_y: wall_choices.append({ "dir": "RIGHT", "y_range": range(min_y, max_y + 1), "x": exit_room.x + exit_room.w - 2, # Right wall is 2 tiles wide "tile_start": STAIRS_RIGHT_START, "w": 2, "h": 3 }) if wall_choices.size() == 0: LogManager.log_error("DungeonGenerator: ERROR - No valid walls for stairs! Exit room too small: " + str(exit_room.w) + "x" + str(exit_room.h), LogManager.CATEGORY_DUNGEON) return {} # No valid walls for stairs # Choose a random wall var wall = wall_choices[rng.randi() % wall_choices.size()] if wall.dir == "UP" or wall.dir == "DOWN": # Horizontal stairs (3x2) if wall.x_range.size() == 0: LogManager.log_error("DungeonGenerator: ERROR - x_range is empty for " + str(wall.dir) + " stairs", LogManager.CATEGORY_DUNGEON) return {} # Try to find a position that doesn't overlap doors var valid_positions = [] for test_x in wall.x_range: var test_stairs_start_x = test_x - 1 # Start 1 tile to the left (3 tiles wide) if not stairs_overlaps_door.call(test_stairs_start_x, wall.y, wall.w, wall.h): valid_positions.append(test_x) if valid_positions.size() == 0: LogManager.log_error("DungeonGenerator: ERROR - No valid position found for " + str(wall.dir) + " stairs (all positions overlap doors)", LogManager.CATEGORY_DUNGEON) # Don't allow stairs to overlap doors - this is a critical bug # Instead, try the next wall or return empty to force placement elsewhere return {} # No valid position found - will trigger _force_place_stairs # Choose random valid position var stairs_x = valid_positions[rng.randi() % valid_positions.size()] var stairs_start_x = stairs_x - 1 # Start 1 tile to the left (3 tiles wide) # Store stairs data for Area2D creation stairs_data = { "x": stairs_start_x, "y": wall.y, "w": wall.w, "h": wall.h, "dir": wall.dir, "world_pos": Vector2((stairs_start_x + wall.w / 2.0) * tile_size, (wall.y + wall.h / 2.0) * tile_size), "world_size": Vector2(wall.w * tile_size, wall.h * tile_size) } LogManager.log("DungeonGenerator: Placed " + str(wall.dir) + " stairs at tile (" + str(stairs_start_x) + "," + str(wall.y) + ") world pos: " + str(stairs_data.world_pos) + " in room (" + str(exit_room.x) + "," + str(exit_room.y) + ") size " + str(exit_room.w) + "x" + str(exit_room.h), LogManager.CATEGORY_DUNGEON) # Mark grid cells as stairs (similar to doors) for dx in range(wall.w): for dy in range(wall.h): var x = stairs_start_x + dx var y = wall.y + dy if x >= 0 and x < map_size.x and y >= 0 and y < map_size.y: grid[x][y] = 4 # Stairs (use grid value 4) # Render stairs tiles (similar to doors but with different middle frame) if wall.dir == "UP": if dy == 0: # First row if dx == 1: # Middle tile tile_grid[x][y] = Vector2i(10, 0) # Special middle tile for UP stairs else: tile_grid[x][y] = wall.tile_start + Vector2i(dx, dy) else: tile_grid[x][y] = wall.tile_start + Vector2i(dx, dy) else: # DOWN # For DOWN stairs, use same tiles as door DOWN # Row 0 (dy=0): use door tiles # Row 1 (dy=1): use door tiles, except middle tile (col 1, row 1) which is 6,6 if dy == 1 and dx == 1: # Second row, middle column (col 1, row 1) tile_grid[x][y] = Vector2i(6, 6) # Special middle tile for DOWN stairs else: # Use door DOWN tiles (same as DOOR_BOTTOM_START = 7,5) tile_grid[x][y] = wall.tile_start + Vector2i(dx, dy) elif wall.dir == "LEFT" or wall.dir == "RIGHT": # Vertical stairs (2x3) if wall.y_range.size() == 0: LogManager.log_error("DungeonGenerator: ERROR - y_range is empty for " + str(wall.dir) + " stairs", LogManager.CATEGORY_DUNGEON) return {} # Try to find a position that doesn't overlap doors var valid_positions = [] for test_y in wall.y_range: var test_stairs_start_y = test_y - 1 # Start 1 tile up (3 tiles tall) if not stairs_overlaps_door.call(wall.x, test_stairs_start_y, wall.w, wall.h): valid_positions.append(test_y) if valid_positions.size() == 0: LogManager.log_error("DungeonGenerator: ERROR - No valid position found for " + str(wall.dir) + " stairs (all positions overlap doors)", LogManager.CATEGORY_DUNGEON) # Don't allow stairs to overlap doors - this is a critical bug # Instead, try the next wall or return empty to force placement elsewhere return {} # No valid position found - will trigger _force_place_stairs # Choose random valid position var stairs_y = valid_positions[rng.randi() % valid_positions.size()] var stairs_start_y = stairs_y - 1 # Start 1 tile up (3 tiles tall) # Store stairs data for Area2D creation stairs_data = { "x": wall.x, "y": stairs_start_y, "w": wall.w, "h": wall.h, "dir": wall.dir, "world_pos": Vector2((wall.x + wall.w / 2.0) * tile_size, (stairs_start_y + wall.h / 2.0) * tile_size), "world_size": Vector2(wall.w * tile_size, wall.h * tile_size) } LogManager.log("DungeonGenerator: Placed " + str(wall.dir) + " stairs at tile (" + str(wall.x) + "," + str(stairs_start_y) + ") world pos: " + str(stairs_data.world_pos) + " in room (" + str(exit_room.x) + "," + str(exit_room.y) + ") size " + str(exit_room.w) + "x" + str(exit_room.h), LogManager.CATEGORY_DUNGEON) # Mark grid cells as stairs for dx in range(wall.w): for dy in range(wall.h): var x = wall.x + dx var y = stairs_start_y + dy if x >= 0 and x < map_size.x and y >= 0 and y < map_size.y: grid[x][y] = 4 # Stairs # Render stairs tiles with special middle frame if wall.dir == "LEFT": if dx == 0: # First column if dy == 1: # Middle tile (second row) tile_grid[x][y] = Vector2i(5, 1) # Special middle tile for LEFT stairs else: tile_grid[x][y] = wall.tile_start + Vector2i(dx, dy) else: tile_grid[x][y] = wall.tile_start + Vector2i(dx, dy) else: # RIGHT if dx == 1: # Second column if dy == 1: # Middle tile (second row) tile_grid[x][y] = Vector2i(11, 1) # Special middle tile for RIGHT stairs else: tile_grid[x][y] = wall.tile_start + Vector2i(dx, dy) else: tile_grid[x][y] = wall.tile_start + Vector2i(dx, dy) return stairs_data func _force_place_stairs(exit_room: Dictionary, grid: Array, tile_grid: Array, map_size: Vector2i, all_doors: Array, _rng: RandomNumberGenerator) -> Dictionary: # Force place stairs in exit room - used as fallback when normal placement fails # Still tries to avoid door overlaps, but will place stairs even if room is small # Uses same positioning logic as doors: at least 2 tiles from corners LogManager.log("DungeonGenerator: Force placing stairs in exit room: " + str(exit_room.x) + "," + str(exit_room.y) + " size: " + str(exit_room.w) + "x" + str(exit_room.h), LogManager.CATEGORY_DUNGEON) var stairs_data: Dictionary = {} var tile_size = 16 # Helper function to check if stairs position overlaps with any door (same as in _place_stairs_in_exit_room) # CRITICAL: Check each stairs tile against each door tile to ensure no overlap # Uses both door dictionary AND grid check for reliability var stairs_overlaps_door = func(test_x: int, test_y: int, test_w: int, test_h: int) -> bool: # FIRST: Check grid - if any stairs tile is marked as a door (grid value 2), it's an overlap for stairs_tile_x in range(test_x, test_x + test_w): for stairs_tile_y in range(test_y, test_y + test_h): if stairs_tile_x >= 0 and stairs_tile_x < map_size.x and stairs_tile_y >= 0 and stairs_tile_y < map_size.y: if grid[stairs_tile_x][stairs_tile_y] == 2: # Grid value 2 = door return true # SECOND: Check door dictionary - verify against all known doors for stairs_tile_x in range(test_x, test_x + test_w): for stairs_tile_y in range(test_y, test_y + test_h): # Check this stairs tile against all door tiles from door dictionary for door in all_doors: var door_x = door.x var door_y = door.y # Determine actual door tile positions based on direction var door_tiles: Array = [] # Array of {x, y} for each door tile if "dir" in door: match door.dir: "E", "W": # Horizontal doors (E/W): 2x3 tiles (2 wide, 3 tall) for door_dx in range(2): for door_dy in range(3): door_tiles.append({"x": door_x + door_dx, "y": door_y + door_dy}) "N", "S": # Vertical doors (N/S): 3x2 tiles (3 wide, 2 tall) for door_dx in range(3): for door_dy in range(2): door_tiles.append({"x": door_x + door_dx, "y": door_y + door_dy}) _: # Fallback: assume 3x2 (vertical) for door_dx in range(3): for door_dy in range(2): door_tiles.append({"x": door_x + door_dx, "y": door_y + door_dy}) else: # Fallback: assume 3x2 (vertical) for door_dx in range(3): for door_dy in range(2): door_tiles.append({"x": door_x + door_dx, "y": door_y + door_dy}) # Check if this stairs tile matches any door tile for door_tile in door_tiles: if stairs_tile_x == door_tile.x and stairs_tile_y == door_tile.y: return true return false # Calculate safe position for stairs (3 tiles wide, 2 tiles tall) # Try to place on top wall first, then bottom, then left, then right, then center floor var stairs_x = 0 var stairs_y = 0 var stairs_w = 3 var stairs_h = 2 var stairs_dir = "UP" var found_position = false # Try top wall (preferred) - use same logic as doors # Minimum room width: 2 (left buffer) + 3 (stairs) + 2 (right buffer) = 7 tiles if exit_room.w >= 7: var min_x = exit_room.x + 2 # At least 2 tiles from left corner var max_x = exit_room.x + exit_room.w - 5 # At least 5 tiles from right edge (2 buffer + 3 stairs) # Try multiple positions to avoid doors for test_x in range(min_x, max_x + 1): var test_stairs_start_x = test_x - 1 # Start 1 tile to the left (3 tiles wide) if not stairs_overlaps_door.call(test_stairs_start_x, exit_room.y, stairs_w, stairs_h): stairs_x = test_stairs_start_x stairs_y = exit_room.y stairs_dir = "UP" found_position = true break # Don't place if all positions overlap - try next wall instead # Try bottom wall if not found_position and exit_room.w >= 7: var min_x = exit_room.x + 2 var max_x = exit_room.x + exit_room.w - 5 for test_x in range(min_x, max_x + 1): var test_stairs_start_x = test_x - 1 if not stairs_overlaps_door.call(test_stairs_start_x, exit_room.y + exit_room.h - stairs_h, stairs_w, stairs_h): stairs_x = test_stairs_start_x stairs_y = exit_room.y + exit_room.h - stairs_h stairs_dir = "DOWN" found_position = true break # Don't place if all positions overlap - try next wall instead # Try left wall (vertical stairs) # Minimum room height: 2 (top buffer) + 3 (stairs) + 2 (bottom buffer) = 7 tiles if not found_position and exit_room.h >= 7: var min_y = exit_room.y + 2 var max_y = exit_room.y + exit_room.h - 5 stairs_w = 2 stairs_h = 3 for test_y in range(min_y, max_y + 1): var test_stairs_start_y = test_y - 1 # Start 1 tile up (3 tiles tall) if not stairs_overlaps_door.call(exit_room.x, test_stairs_start_y, stairs_w, stairs_h): stairs_x = exit_room.x stairs_y = test_stairs_start_y stairs_dir = "LEFT" found_position = true break # Don't place if all positions overlap - try next wall instead # Try right wall (vertical stairs) if not found_position and exit_room.h >= 7: var min_y = exit_room.y + 2 var max_y = exit_room.y + exit_room.h - 5 stairs_w = 2 stairs_h = 3 for test_y in range(min_y, max_y + 1): var test_stairs_start_y = test_y - 1 if not stairs_overlaps_door.call(exit_room.x + exit_room.w - 2, test_stairs_start_y, stairs_w, stairs_h): stairs_x = exit_room.x + exit_room.w - 2 stairs_y = test_stairs_start_y stairs_dir = "RIGHT" found_position = true break # Don't place if all positions overlap - try next wall instead # Last resort: place in center of room floor (any room size) # But ONLY if it doesn't overlap any doors! if not found_position: stairs_w = 3 stairs_h = 2 var center_x = exit_room.x + max(1, (exit_room.w - stairs_w) / 2) var center_y = exit_room.y + max(1, (exit_room.h - stairs_h) / 2) center_x = clamp(center_x, exit_room.x + 1, exit_room.x + exit_room.w - stairs_w - 1) center_y = clamp(center_y, exit_room.y + 1, exit_room.y + exit_room.h - stairs_h - 1) # Check if center position overlaps any doors if not stairs_overlaps_door.call(center_x, center_y, stairs_w, stairs_h): stairs_x = center_x stairs_y = center_y stairs_dir = "UP" found_position = true else: # Try to find ANY free position in the room that doesn't overlap doors for test_y in range(exit_room.y + 1, exit_room.y + exit_room.h - stairs_h): for test_x in range(exit_room.x + 1, exit_room.x + exit_room.w - stairs_w): if not stairs_overlaps_door.call(test_x, test_y, stairs_w, stairs_h): stairs_x = test_x stairs_y = test_y stairs_dir = "UP" found_position = true break if found_position: break # If still no valid position found, return empty (don't place stairs that overlap doors!) if not found_position: LogManager.log_error("DungeonGenerator: ERROR - Could not find any position for stairs that doesn't overlap doors!", LogManager.CATEGORY_DUNGEON) return {} stairs_data = { "x": stairs_x, "y": stairs_y, "w": stairs_w, "h": stairs_h, "dir": stairs_dir, "world_pos": Vector2((stairs_x + stairs_w / 2.0) * tile_size, (stairs_y + stairs_h / 2.0) * tile_size), "world_size": Vector2(stairs_w * tile_size, stairs_h * tile_size) } # Mark grid cells as stairs for dx in range(stairs_data.w): for dy in range(stairs_data.h): var x = stairs_data.x + dx var y = stairs_data.y + dy if x >= 0 and x < map_size.x and y >= 0 and y < map_size.y: grid[x][y] = 4 # Stairs # Render stairs tiles based on direction (same as normal placement) if stairs_dir == "UP": if dy == 0: # First row if dx == 1: # Middle tile tile_grid[x][y] = Vector2i(10, 0) # Special middle tile for UP stairs else: tile_grid[x][y] = STAIRS_UP_START + Vector2i(dx, dy) else: tile_grid[x][y] = STAIRS_UP_START + Vector2i(dx, dy) elif stairs_dir == "DOWN": # For DOWN stairs, use same tiles as door DOWN # Row 0 (dy=0): use door tiles # Row 1 (dy=1): use door tiles, except middle tile (col 1, row 1) which is 6,6 if dy == 1 and dx == 1: # Second row, middle column (col 1, row 1) tile_grid[x][y] = Vector2i(6, 6) # Special middle tile for DOWN stairs else: # Use door DOWN tiles (same as DOOR_BOTTOM_START = STAIRS_DOWN_START = 7,5) tile_grid[x][y] = STAIRS_DOWN_START + Vector2i(dx, dy) elif stairs_dir == "LEFT": # Vertical stairs on left wall if dx == 0: # First column if dy == 1: # Middle tile (second row) tile_grid[x][y] = Vector2i(5, 1) # Special middle tile for LEFT stairs else: tile_grid[x][y] = STAIRS_LEFT_START + Vector2i(dx, dy) else: tile_grid[x][y] = STAIRS_LEFT_START + Vector2i(dx, dy) elif stairs_dir == "RIGHT": # Vertical stairs on right wall if dx == 1: # Second column if dy == 1: # Middle tile (second row) tile_grid[x][y] = Vector2i(11, 1) # Special middle tile for RIGHT stairs else: tile_grid[x][y] = STAIRS_RIGHT_START + Vector2i(dx, dy) else: tile_grid[x][y] = STAIRS_RIGHT_START + Vector2i(dx, dy) else: # Fallback: use UP stairs tiles tile_grid[x][y] = STAIRS_UP_START + Vector2i(dx, dy) LogManager.log("DungeonGenerator: Force placed " + str(stairs_dir) + " stairs at tile (" + str(stairs_data.x) + "," + str(stairs_data.y) + ") world pos: " + str(stairs_data.world_pos), LogManager.CATEGORY_DUNGEON) return stairs_data func _place_interactable_objects_in_room(room: Dictionary, grid: Array, map_size: Vector2i, all_doors: Array, all_enemies: Array, rng: RandomNumberGenerator, room_puzzle_data: Dictionary = {}) -> Array: # Place interactable objects in a room # Small rooms (7-8 tiles): 0-1 objects # Medium rooms (9-10 tiles): 0-3 objects # Large rooms (11-12 tiles): 0-8 objects # Returns array of interactable object data dictionaries # CRITICAL: If room has "switch_pillar" puzzle, MUST spawn at least 1 Pillar (regardless of room size) var objects = [] var tile_size = 16 # Check if room has a "switch_pillar" puzzle - if so, we MUST spawn at least 1 pillar var has_pillar_switch_puzzle = false var matching_puzzle_room = null if room_puzzle_data.size() > 0: # Try direct lookup first (room dictionary as key) if room_puzzle_data.has(room): matching_puzzle_room = room else: # Fallback: find matching room by comparing values (x, y, w, h) for puzzle_room in room_puzzle_data.keys(): # Compare rooms by values (x, y, w, h) if puzzle_room.x == room.x and puzzle_room.y == room.y and \ puzzle_room.w == room.w and puzzle_room.h == room.h: matching_puzzle_room = puzzle_room break if matching_puzzle_room != null: var puzzle_info = room_puzzle_data[matching_puzzle_room] LogManager.log("DungeonGenerator: Checking room (" + str(room.x) + "," + str(room.y) + ") - puzzle_room (" + str(matching_puzzle_room.x) + "," + str(matching_puzzle_room.y) + ") puzzle_type: " + str(puzzle_info.type), LogManager.CATEGORY_DUNGEON) if puzzle_info.type == "switch_pillar": has_pillar_switch_puzzle = true LogManager.log("DungeonGenerator: Room (" + str(room.x) + "," + str(room.y) + ") has pillar switch puzzle - will spawn at least 1 pillar", LogManager.CATEGORY_DUNGEON) else: LogManager.log("DungeonGenerator: room_puzzle_data is empty for room (" + str(room.x) + "," + str(room.y) + ")", LogManager.CATEGORY_DUNGEON) # Calculate room floor area (excluding walls) var floor_w = room.w - 4 # Excluding 2-tile walls on each side var floor_h = room.h - 4 var floor_area = floor_w * floor_h # Determine max objects based on room size var max_objects: int = 0 if floor_area <= 16: # Small rooms (4x4 or smaller floor) max_objects = 1 elif floor_area <= 36: # Medium rooms (up to 6x6 floor) max_objects = 3 else: # Large rooms (7x7+ floor) max_objects = 8 var num_objects = rng.randi_range(0, max_objects) # CRITICAL: If room has pillar switch puzzle, ensure we spawn at least 1 pillar # This MUST happen regardless of room size if has_pillar_switch_puzzle: # Set minimum to 1 if num_objects is 0 if num_objects == 0: num_objects = 1 # The pillar will be placed FIRST in the objects list (before any other objects) # Available object types and their setup functions var object_types = [ {"type": "Pot", "setup": "setup_pot"}, {"type": "LiftableBarrel", "setup": "setup_liftable_barrel"}, {"type": "PushableBarrel", "setup": "setup_pushable_barrel"}, {"type": "Box", "setup": "setup_box"}, {"type": "Chest", "setup": "setup_chest"}, {"type": "Pillar", "setup": "setup_pillar"}, {"type": "PushableHighBox", "setup": "setup_pushable_high_box"} ] # Find valid floor positions in the room (excluding walls, enemies, and door areas) var valid_positions = [] # Room interior is from room.x + 2 to room.x + room.w - 2 (excluding 2-tile walls) # CRITICAL: Also exclude last row/column of floor tiles to prevent objects from spawning in walls # Objects are 16x16, so we need at least 1 tile buffer from walls # Floor tiles are at: room.x+2, room.x+3, ..., room.x+room.w-3 (last floor tile before wall) # To exclude last tile, we want: room.x+2, room.x+3, ..., room.x+room.w-4 var min_x = room.x + 2 var max_x = room.x + room.w - 4 # Exclude last 2 floor tiles (room.w-3 is last floor, room.w-4 is second-to-last) var min_y = room.y + 2 var max_y = room.y + room.h - 4 # Exclude last 2 floor tiles (room.h-3 is last floor, room.h-4 is second-to-last) for x in range(min_x, max_x + 1): # +1 because range is exclusive at end for y in range(min_y, max_y + 1): if x >= 0 and x < map_size.x and y >= 0 and y < map_size.y: # Check if it's a floor tile if 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) # Check if position is valid (not blocked by door, not occupied by enemy) if _is_valid_interactable_position(world_pos, all_doors, all_enemies, room): valid_positions.append(world_pos) # Early return if no valid positions (unless pillar is required, but that's handled below) if valid_positions.size() == 0: if has_pillar_switch_puzzle: # CRITICAL: Pillar is REQUIRED, so we must find at least one position # Try a more permissive search - use ALL floor tiles in the room (even if near doors/walls) LogManager.log("DungeonGenerator: Room (" + str(room.x) + "," + str(room.y) + ") has pillar switch puzzle but no valid positions. Trying fallback search...", LogManager.CATEGORY_DUNGEON) # Use same bounds but skip the position validation check var found_fallback = false for x in range(min_x, max_x + 1): for y in range(min_y, max_y + 1): if x >= 0 and x < map_size.x and y >= 0 and y < map_size.y: if grid[x][y] == 1: # Floor var world_x = x * tile_size + 8 var world_y = y * tile_size + 8 var world_pos = Vector2(world_x, world_y) valid_positions.append(world_pos) # Only need one position for the required pillar found_fallback = true break if found_fallback: break if valid_positions.size() == 0: push_warning("DungeonGenerator: Room (", room.x, ",", room.y, ") has pillar switch puzzle but NO floor tiles! Cannot place pillar.") return objects else: LogManager.log("DungeonGenerator: Found fallback position for required pillar in room (" + str(room.x) + "," + str(room.y) + ")", LogManager.CATEGORY_DUNGEON) else: # No pillar required, safe to return return objects # Shuffle positions to randomize placement valid_positions.shuffle() # CRITICAL: If room has pillar switch puzzle, the FIRST object MUST be a Pillar if has_pillar_switch_puzzle: # Place pillar as the first object var pillar_type_data = {"type": "Pillar", "setup": "setup_pillar"} objects.append({ "type": pillar_type_data.type, "setup_function": pillar_type_data.setup, "position": valid_positions[0], "room": room }) # Place remaining objects (skip first position since it's used by the pillar) # We need to place num_objects - 1 more objects (since pillar counts as 1) var remaining_objects = num_objects - 1 var positions_index = 1 # Start from second position for i in range(min(remaining_objects, valid_positions.size() - 1)): if positions_index >= valid_positions.size(): break # No more valid positions var object_type_data = object_types[rng.randi() % object_types.size()] # Skip Pillar type for remaining objects (already placed one) while object_type_data.type == "Pillar": object_type_data = object_types[rng.randi() % object_types.size()] objects.append({ "type": object_type_data.type, "setup_function": object_type_data.setup, "position": valid_positions[positions_index], "room": room }) positions_index += 1 else: # Normal placement: no pillar requirement for i in range(min(num_objects, valid_positions.size())): var object_type_data = object_types[rng.randi() % object_types.size()] var position = valid_positions[i] objects.append({ "type": object_type_data.type, "setup_function": object_type_data.setup, "position": position, "room": room }) return objects func _is_valid_interactable_position(world_pos: Vector2, all_doors: Array, all_enemies: Array, room: Dictionary) -> bool: # Check if position is not blocked by a door or occupied by an enemy var tile_size = 16 var tile_x = int(world_pos.x / tile_size) var tile_y = int(world_pos.y / tile_size) # Check if position is in front of a door (within 2 tiles of door center) for door in all_doors: var door_x = door.x var door_y = door.y # Determine actual door dimensions based on direction var door_w: int var door_h: int if "dir" in door: match door.dir: "E", "W": # Horizontal doors: 2x3 door_w = 2 door_h = 3 "N", "S": # Vertical doors: 3x2 door_w = 3 door_h = 2 _: door_w = door.w if "w" in door else 3 door_h = door.h if "h" in door else 2 else: door_w = 3 door_h = 2 # Check if position is within door area if tile_x >= door_x and tile_x < door_x + door_w and \ tile_y >= door_y and tile_y < door_y + door_h: return false # Check if position is within 2 tiles in front of door (based on door direction) # Check if door connects to this room var door_connects_to_room = false if "room1" in door and door.room1 == room: door_connects_to_room = true elif "room2" in door and door.room2 == room: door_connects_to_room = true if door_connects_to_room: # Check if position is within 2 tiles in front of door if door.dir == "E" or door.dir == "W": # Horizontal door if door.dir == "E": # Door on right wall, position should be to the left (inside room) if tile_x >= door_x - 2 and tile_x < door_x and \ tile_y >= door_y - 1 and tile_y < door_y + door_h + 1: return false else: # W # Door on left wall, position should be to the right (inside room) if tile_x > door_x + door_w and tile_x <= door_x + door_w + 2 and \ tile_y >= door_y - 1 and tile_y < door_y + door_h + 1: return false else: # N or S # Vertical door if door.dir == "S": # Door on bottom wall, position should be above (inside room) if tile_y >= door_y - 2 and tile_y < door_y and \ tile_x >= door_x - 1 and tile_x < door_x + door_w + 1: return false else: # N # Door on top wall, position should be below (inside room) if tile_y > door_y + door_h and tile_y <= door_y + door_h + 2 and \ tile_x >= door_x - 1 and tile_x < door_x + door_w + 1: return false # Check if position is occupied by an enemy for enemy in all_enemies: if enemy.has("position") and enemy.position is Vector2: var enemy_tile_x = int(enemy.position.x / tile_size) var enemy_tile_y = int(enemy.position.y / tile_size) # Check if within 1 tile (enemies and objects shouldn't overlap) if abs(tile_x - enemy_tile_x) <= 1 and abs(tile_y - enemy_tile_y) <= 1: return false return true func _find_rooms_before_door(door: Dictionary, start_room: Dictionary, _all_rooms: Array, all_doors: Array) -> Array: # Find rooms that are reachable from start WITHOUT going through this door # This is used to place keys before KeyDoors var rooms_before_door = [] var visited = [start_room] var queue = [start_room] while queue.size() > 0: var current = queue.pop_front() # Add current room to result (it's reachable before the door) # Don't include the rooms the door connects (need key before reaching them) if current != door.room1 and current != door.room2: if not rooms_before_door.has(current): rooms_before_door.append(current) # Check all doors connected to current room (except the blocked door) for d in all_doors: if d == door: continue # Skip the blocked door var next_room = null if d.room1 == current: next_room = d.room2 elif d.room2 == current: next_room = d.room1 if next_room != null and not visited.has(next_room): visited.append(next_room) queue.append(next_room) return rooms_before_door func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_size: Vector2i, rng: RandomNumberGenerator, start_room_index: int, exit_room_index: int) -> Dictionary: # Place blocking doors on existing tile doors # Returns array of blocking door data dictionaries var blocking_doors = [] var tile_size = 16 # Get start and exit room references var start_room = all_rooms[start_room_index] var _exit_room = all_rooms[exit_room_index] # Calculate reachability from start room to determine where keys can be placed var _reachable_rooms_from_start = _find_reachable_rooms(start_room, all_rooms, all_doors) # Track which rooms have puzzles and which doors are already assigned var _rooms_with_puzzles = {} # room -> true var assigned_doors = [] # Doors already assigned to a room puzzle var room_puzzle_data = {} # room -> {type: "switch" or "enemy", doors: []} # STEP 1: For each room (except start/exit), randomly decide if it has a door-puzzle var puzzle_room_chance = 0.4 # 40% chance per room LogManager.log("DungeonGenerator: Assigning puzzles to rooms (" + str(all_rooms.size()) + " total rooms, excluding start/exit)", LogManager.CATEGORY_DUNGEON) for i in range(all_rooms.size()): if i == start_room_index or i == exit_room_index: continue # Skip start and exit rooms var room = all_rooms[i] if rng.randf() < puzzle_room_chance: LogManager.log("DungeonGenerator: Room (" + str(room.x) + ", " + str(room.y) + ") selected for puzzle assignment", LogManager.CATEGORY_DUNGEON) # This room has a puzzle! # CRITICAL SAFETY CHECK: Never assign puzzles to start or exit rooms # Double-check even though we skip them in the loop if i == start_room_index or i == exit_room_index: continue # Find all doors that are connected to this puzzle room var doors_in_room = [] for door in all_doors: var door_room1 = door.room1 if ("room1" in door and door.room1 and not door.room1.is_empty()) else null var door_room2 = door.room2 if ("room2" in door and door.room2 and not door.room2.is_empty()) else null var room_matches = false # Check if room1 matches puzzle room if door_room1: room_matches = (door_room1.x == room.x and door_room1.y == room.y and \ door_room1.w == room.w and door_room1.h == room.h) # Check if room2 matches puzzle room if not room_matches and door_room2: room_matches = (door_room2.x == room.x and door_room2.y == room.y and \ door_room2.w == room.w and door_room2.h == room.h) # Door is connected to puzzle room if room_matches: doors_in_room.append(door) if doors_in_room.size() == 0: LogManager.log("DungeonGenerator: Room (" + str(room.x) + ", " + str(room.y) + ") has no doors connected - skipping puzzle assignment", LogManager.CATEGORY_DUNGEON) continue # No doors connected to this room, skip LogManager.log("DungeonGenerator: Room (" + str(room.x) + ", " + str(room.y) + ") has " + str(doors_in_room.size()) + " doors - assigning puzzle", LogManager.CATEGORY_DUNGEON) # Decide puzzle type: 33% walk switch, 33% pillar switch, 33% enemy spawner (if room is large enough) var can_have_enemies = false var interior_width = room.w - 4 # Exclude 2-tile walls var interior_height = room.h - 4 can_have_enemies = (interior_width >= 5 and interior_height >= 5) var puzzle_type = "" var rand_val = rng.randf() if can_have_enemies and rand_val < 0.33: puzzle_type = "enemy" elif rand_val < 0.66: puzzle_type = "switch_walk" else: puzzle_type = "switch_pillar" # Store puzzle data for this room room_puzzle_data[room] = { "type": puzzle_type, "doors": doors_in_room } LogManager.log("DungeonGenerator: Stored puzzle data for room (" + str(room.x) + ", " + str(room.y) + ") - type: " + str(puzzle_type) + ", doors: " + str(doors_in_room.size()), LogManager.CATEGORY_DUNGEON) # Mark these doors as assigned for door in doors_in_room: assigned_doors.append(door) LogManager.log("DungeonGenerator: Assigned puzzles to " + str(room_puzzle_data.size()) + " rooms", LogManager.CATEGORY_DUNGEON) # STEP 2: Create blocking doors for rooms with puzzles # CRITICAL: Blocking doors should ONLY be placed ON THE DOORS IN THE PUZZLE ROOM # NEVER create blocking doors for rooms that are NOT in room_puzzle_data! # When you enter the puzzle room, these doors close, trapping you until you solve the puzzle for room in room_puzzle_data.keys(): # CRITICAL SAFETY CHECK #1: Verify this room is actually in room_puzzle_data if not room in room_puzzle_data: LogManager.log_error("DungeonGenerator: ERROR - Room (" + str(room.x) + ", " + str(room.y) + ") is NOT in room_puzzle_data! This should never happen!", LogManager.CATEGORY_DUNGEON) continue # CRITICAL SAFETY CHECK #2: Never create blocking doors for start or exit rooms var room_index = -1 for j in range(all_rooms.size()): var check_room = all_rooms[j] if check_room.x == room.x and check_room.y == room.y and \ check_room.w == room.w and check_room.h == room.h: room_index = j break if room_index == start_room_index or room_index == exit_room_index: LogManager.log_error("DungeonGenerator: ERROR - Attempted to create blocking doors for start/exit room! Skipping.", LogManager.CATEGORY_DUNGEON) continue # CRITICAL SAFETY CHECK #3: Verify this room is actually in all_rooms (sanity check) if room_index == -1: LogManager.log_error("DungeonGenerator: ERROR - Room (" + str(room.x) + ", " + str(room.y) + ") not found in all_rooms! Skipping.", LogManager.CATEGORY_DUNGEON) continue var puzzle_info = room_puzzle_data[room] var doors_in_room = puzzle_info.doors # Doors that are IN this puzzle room (lead OUT OF it) var puzzle_type = puzzle_info.type if doors_in_room.size() == 0: LogManager.log("DungeonGenerator: WARNING - Room has puzzle but no doors! Skipping.", LogManager.CATEGORY_DUNGEON) continue # Randomly choose door type: 50% StoneDoor, 50% GateDoor var door_type = "StoneDoor" if rng.randf() < 0.5 else "GateDoor" # Create puzzle element first (switch or spawner) - ONCE per room, shared by all doors var puzzle_element_created = false var puzzle_element_data = {} if puzzle_type == "switch_walk" or puzzle_type == "switch_pillar": # Find a valid floor position for switch IN THE PUZZLE ROOM var switch_type = "walk" if puzzle_type == "switch_walk" else "pillar" var switch_weight = 1.0 if switch_type == "walk" else 5.0 var switch_data = _find_floor_switch_position(room, grid, map_size, rng, -1, -1) if switch_data != null and not switch_data.is_empty() and switch_data.has("position"): puzzle_element_created = true puzzle_element_data = { "type": "switch", "switch_type": switch_type, "switch_weight": switch_weight, "switch_data": switch_data, "switch_room": room } LogManager.log("DungeonGenerator: Created switch puzzle for room (" + str(room.x) + ", " + str(room.y) + ") - type: " + str(switch_type), LogManager.CATEGORY_DUNGEON) else: LogManager.log("DungeonGenerator: WARNING - Could not place floor switch in puzzle room (" + str(room.x) + ", " + str(room.y) + ")! Skipping puzzle.", LogManager.CATEGORY_DUNGEON) elif puzzle_type == "enemy": # Add enemy spawner IN THE PUZZLE ROOM var spawner_positions = [] 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: if 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 spawner_positions.append({ "position": Vector2(world_x, world_y), "tile_x": x, "tile_y": y }) if spawner_positions.size() > 0: spawner_positions.shuffle() var spawner_data = spawner_positions[0] puzzle_element_created = true puzzle_element_data = { "type": "enemy", "spawner_data": spawner_data, "spawner_room": room } LogManager.log("DungeonGenerator: Created enemy spawner puzzle for room (" + str(room.x) + ", " + str(room.y) + ") - spawner at " + str(spawner_data.position), LogManager.CATEGORY_DUNGEON) else: LogManager.log("DungeonGenerator: WARNING - Could not place enemy spawner in puzzle room (" + str(room.x) + ", " + str(room.y) + ")! Skipping puzzle.", LogManager.CATEGORY_DUNGEON) # CRITICAL SAFETY CHECK: Only create blocking doors if puzzle element was successfully created if not puzzle_element_created: LogManager.log_error("DungeonGenerator: ERROR - Puzzle element was NOT created for room (" + str(room.x) + ", " + str(room.y) + ") with puzzle_type: " + str(puzzle_type) + "! Skipping ALL doors in this room.", LogManager.CATEGORY_DUNGEON) # Remove doors from assigned list since we're not creating the puzzle for door in doors_in_room: if door in assigned_doors: assigned_doors.erase(door) continue # CRITICAL: Verify puzzle_element_data is valid before proceeding if puzzle_element_data.is_empty() or not puzzle_element_data.has("type"): LogManager.log_error("DungeonGenerator: ERROR - puzzle_element_data is invalid for room (" + str(room.x) + ", " + str(room.y) + ")! puzzle_element_created was true but data is empty!", LogManager.CATEGORY_DUNGEON) continue # Create blocking doors for at least 1 door (minimum), or all doors in the room # For now, create blocking doors for ALL doors in the puzzle room LogManager.log("DungeonGenerator: Creating blocking doors for room (" + str(room.x) + ", " + str(room.y) + ") with " + str(doors_in_room.size()) + " doors, puzzle type: " + str(puzzle_type) + ", puzzle_element type: " + str(puzzle_element_data.type), LogManager.CATEGORY_DUNGEON) for door in doors_in_room: # Determine direction based on which WALL of the PUZZLE ROOM the door is on var direction = _determine_door_direction_for_puzzle_room(door, room, all_rooms) # CRITICAL: door.x and door.y are the position in room1, not necessarily in puzzle room # Need to calculate the correct position on the puzzle room's wall var door_room2 = door.room2 if ("room2" in door and door.room2 and not door.room2.is_empty()) else null # Determine if puzzle room is room2 (if so, door position needs adjustment) var puzzle_is_room2 = false if door_room2: puzzle_is_room2 = (door_room2.x == room.x and door_room2.y == room.y and \ door_room2.w == room.w and door_room2.h == room.h) # Calculate door position on the puzzle room's wall var door_tile_x = door.x var door_tile_y = door.y var open_tile_x = door_tile_x var open_tile_y = door_tile_y # If puzzle room is room2, the door position needs to be moved to the puzzle room's wall if puzzle_is_room2: # Door is connecting from room1 to puzzle room (room2) # We need to calculate the position on puzzle room's wall based on door direction match direction: "Up": # Door on top wall of puzzle room - door.y should be at puzzle_room.y open_tile_x = door_tile_x # Keep same X (horizontal position) open_tile_y = room.y # Top wall of puzzle room "Down": # Door on bottom wall of puzzle room - door.y should be at puzzle_room.y + room.h - 1 open_tile_x = door_tile_x # Keep same X (horizontal position) open_tile_y = room.y + room.h - 1 # Bottom wall of puzzle room "Left": # Door on left wall of puzzle room - door.x should be at puzzle_room.x open_tile_x = room.x # Left wall of puzzle room open_tile_y = door_tile_y # Keep same Y (vertical position) "Right": # Door on right wall of puzzle room - door.x should be at puzzle_room.x + room.w - 1 open_tile_x = room.x + room.w - 1 # Right wall of puzzle room open_tile_y = door_tile_y # Keep same Y (vertical position) # else: Puzzle room is room1 - door position is already in puzzle room, use as-is # Adjust position based on door direction and offset from wall # These offsets are relative to the door's position on the wall match direction: "Up": # Door Up (3x2): Open at middle column, top row # open_tile_x is already on the wall, adjust to middle column open_tile_x = open_tile_x + 1 # Middle column (3 tiles wide, so +1 from left edge) open_tile_y = open_tile_y + 0 # Already at top wall (row 0) "Right": # Door Right (2x3): Open at right column, middle row # open_tile_x is already on the wall, adjust to right column open_tile_x = open_tile_x + 1 # Right column (already at wall) open_tile_y = open_tile_y + 1 # Middle row (3 tiles tall, so +1 from top) "Down": # Door Down (3x2): Open at middle column, bottom row # open_tile_x is already on the wall, adjust to middle column open_tile_x = open_tile_x + 1 # Middle column (3 tiles wide, so +1 from left edge) open_tile_y = open_tile_y + 1 # Bottom row (2 tiles tall, so +1 from top edge) "Left": # Door Left (2x3): Open at left column, middle row # open_tile_x is already on the wall, adjust to left column open_tile_x = open_tile_x + 0 # Left column (already at wall) open_tile_y = open_tile_y + 1 # Middle row (3 tiles tall, so +1 from top) # Calculate world position from open tile (center of tile) # This is the OPEN position - door will start here and move to CLOSED position when entering room var door_world_x = open_tile_x * tile_size + tile_size / 2.0 var door_world_y = open_tile_y * tile_size + tile_size / 2.0 # Create door data # Position is the OPEN state position (will move to CLOSED when entering room) # CRITICAL: Verify room is still a valid puzzle room before creating door if not room in room_puzzle_data: LogManager.log_error("DungeonGenerator: ERROR - Room (" + str(room.x) + ", " + str(room.y) + ") is no longer in room_puzzle_data! Cannot create door.", LogManager.CATEGORY_DUNGEON) continue # NOTE: door_room1 is already declared at line 1933 and verified to match puzzle room at line 1935-1940 # No need to re-declare or re-verify here - the door_in_puzzle_room check above already ensures room1 matches var door_data = { "type": door_type, "direction": direction, "position": Vector2(door_world_x, door_world_y), # OPEN position (tile center) "tile_x": open_tile_x, "tile_y": open_tile_y, "door": door, "blocking_room": room, # CRITICAL: This door is IN the puzzle room (the room that has the puzzle) "is_closed": false, # Start open, close when entering puzzle room "puzzle_type": puzzle_type # "switch_walk", "switch_pillar", or "enemy" } # Store puzzle room as room1 for blocking doors door_data.original_room1 = room # Puzzle room is always room1 for blocking doors LogManager.log("DungeonGenerator: Creating blocking door for puzzle room (" + str(room.x) + ", " + str(room.y) + ") - direction: " + str(direction) + ", open_tile: (" + str(open_tile_x) + "," + str(open_tile_y) + ")", LogManager.CATEGORY_DUNGEON) # CRITICAL: Add puzzle-specific data from the puzzle_element_data created above (shared across all doors in room) # Only add door if puzzle element data is valid var door_has_valid_puzzle = false if puzzle_element_data.has("type") and puzzle_element_data.type == "switch": if puzzle_element_data.has("switch_data") and puzzle_element_data.switch_data.has("position"): door_data.floor_switch_position = puzzle_element_data.switch_data.position door_data.switch_tile_x = puzzle_element_data.switch_data.tile_x door_data.switch_tile_y = puzzle_element_data.switch_data.tile_y door_data.switch_room = puzzle_element_data.switch_room door_data.requires_switch = true door_data.switch_type = puzzle_element_data.switch_type door_data.switch_required_weight = puzzle_element_data.switch_weight door_has_valid_puzzle = true LogManager.log("DungeonGenerator: Added switch data to door - switch at (" + str(door_data.switch_tile_x) + ", " + str(door_data.switch_tile_y) + ")", LogManager.CATEGORY_DUNGEON) elif puzzle_element_data.has("type") and puzzle_element_data.type == "enemy": if puzzle_element_data.has("spawner_data") and puzzle_element_data.spawner_data.has("position"): if not "enemy_spawners" in door_data: door_data.enemy_spawners = [] door_data.enemy_spawners.append({ "position": puzzle_element_data.spawner_data.position, "tile_x": puzzle_element_data.spawner_data.tile_x, "tile_y": puzzle_element_data.spawner_data.tile_y, "room": puzzle_element_data.spawner_room, "spawn_once": true # Only spawn 1 enemy, then destroy spawner }) door_data.requires_enemies = true door_has_valid_puzzle = true LogManager.log("DungeonGenerator: Added enemy spawner data to door - spawner at (" + str(puzzle_element_data.spawner_data.tile_x) + ", " + str(puzzle_element_data.spawner_data.tile_y) + ")", LogManager.CATEGORY_DUNGEON) # CRITICAL SAFETY CHECK: Only add door to blocking doors if it has valid puzzle element if not door_has_valid_puzzle: LogManager.log_error("DungeonGenerator: ERROR - Blocking door for room (" + str(room.x) + ", " + str(room.y) + ") has no valid puzzle element! Skipping door. puzzle_type: " + str(puzzle_type) + ", puzzle_element_data: " + str(puzzle_element_data), LogManager.CATEGORY_DUNGEON) continue # Skip this door - don't add it to blocking_doors # FINAL SAFETY CHECK: Verify door has either requires_switch or requires_enemies set if door_data.type == "StoneDoor" or door_data.type == "GateDoor": var has_switch = door_data.get("requires_switch", false) == true var has_enemies = door_data.get("requires_enemies", false) == true if not has_switch and not has_enemies: LogManager.log_error("DungeonGenerator: ERROR - Blocking door (StoneDoor/GateDoor) has neither requires_switch nor requires_enemies! Door data: " + str(door_data.keys()) + " - SKIPPING DOOR", LogManager.CATEGORY_DUNGEON) continue # Skip this door - it's invalid # FINAL CRITICAL SAFETY CHECK: Verify door's blocking_room matches the puzzle room exactly if door_data.blocking_room.x != room.x or door_data.blocking_room.y != room.y or \ door_data.blocking_room.w != room.w or door_data.blocking_room.h != room.h: LogManager.log_error("DungeonGenerator: ERROR - Door blocking_room (" + str(door_data.blocking_room.x) + "," + str(door_data.blocking_room.y) + ") doesn't match puzzle room (" + str(room.x) + "," + str(room.y) + ")! This door is for wrong room! SKIPPING DOOR", LogManager.CATEGORY_DUNGEON) continue # Skip this door - it's for the wrong room # Add door to blocking doors list ONLY if it has valid puzzle element blocking_doors.append(door_data) LogManager.log("DungeonGenerator: Created blocking door for puzzle room (" + str(room.x) + ", " + str(room.y) + ") - direction: " + str(direction) + ", open tile: (" + str(open_tile_x) + ", " + str(open_tile_y) + "), puzzle_type: " + str(puzzle_type) + ", has_switch: " + str(door_data.get("requires_switch", false)) + ", has_enemies: " + str(door_data.get("requires_enemies", false)), LogManager.CATEGORY_DUNGEON) # STEP 3: Randomly assign some doors as KeyDoors (except start/exit room doors and already assigned doors) var key_door_chance = 0.2 # 20% chance per door var key_doors_to_create = [] for door in all_doors: # Skip if already assigned to a room puzzle if door in assigned_doors: continue # Skip doors connected to start or exit rooms var door_room1 = door.room1 if "room1" in door else null var door_room2 = door.room2 if "room2" in door else null var is_start_or_exit_door = false if door_room1: var room1_index = all_rooms.find(door_room1) if room1_index == start_room_index or room1_index == exit_room_index: is_start_or_exit_door = true if door_room2: var room2_index = all_rooms.find(door_room2) if room2_index == start_room_index or room2_index == exit_room_index: is_start_or_exit_door = true if is_start_or_exit_door: continue if rng.randf() < key_door_chance: key_doors_to_create.append(door) # STEP 4: Create KeyDoors with keys placed BEFORE the keydoor for door in key_doors_to_create: # Determine direction var direction = "" if "dir" in door: match door.dir: "E": direction = "Right" "W": direction = "Left" "N": direction = "Up" "S": direction = "Down" _: direction = _determine_door_direction(door, all_rooms) else: direction = _determine_door_direction(door, all_rooms) # Calculate middle tile position var door_tile_x = door.x var door_tile_y = door.y var middle_tile_x = door_tile_x var middle_tile_y = door_tile_y match direction: "Down": # Door Down (3x2): KeyDoors should be placed on row 0, col 1 (CLOSED state) # Row 0 is the upper row (closer to room interior) - this is where KeyDoors start CLOSED # When opened with key, they move to row 1 (col 1, row 1) - 16px down # BUT: After 180° rotation, we need to adjust Y position UP by 8 pixels (half a tile) # to account for sprite alignment - position will be adjusted in door.gd middle_tile_x = door_tile_x + 1 # col 1 (middle column) middle_tile_y = door_tile_y + 0 # row 0 (upper row - CLOSED state for KeyDoor) "Up": # Door Up (3x2): door spans 2 tiles tall, wall is at top edge # Use the TOP tile (row 0) AT the wall boundary middle_tile_x = door_tile_x + 1 # col 1 (middle column) middle_tile_y = door_tile_y + 1 # row 0 (TOP tile, AT the wall boundary) "Right": # Door Right (2x3): door spans 2 tiles, wall is at right edge # Use the RIGHT tile (col 1) at the wall boundary, not the left tile middle_tile_x = door_tile_x + 0 # col 1 (right column, AT the wall boundary) middle_tile_y = door_tile_y + 1 # row 1 (middle row) "Left": # Door Left (2x3): door spans 2 tiles, wall is at left edge # Use the LEFT tile (col 0) at the wall boundary middle_tile_x = door_tile_x + 1 # col 0 (left column, AT the wall boundary) middle_tile_y = door_tile_y + 1 # row 1 (middle row) var door_world_x = middle_tile_x * tile_size + tile_size / 2.0 var door_world_y = middle_tile_y * tile_size + tile_size / 2.0 # Determine which room this door blocks (the room you're entering into) var door_room1 = door.room1 if "room1" in door else null var door_room2 = door.room2 if "room2" in door else null var blocking_room = door_room2 if door_room2 != null else door_room1 # Find rooms reachable BEFORE this door (for key placement) var rooms_before_door = _find_rooms_before_door(door, start_room, all_rooms, all_doors) # Pick a room for the key (must be reachable before the door) var key_room = null if rooms_before_door.size() > 0: # Exclude start and exit rooms from key placement var key_room_candidates = [] for room in rooms_before_door: var room_index = all_rooms.find(room) if room_index != start_room_index and room_index != exit_room_index: key_room_candidates.append(room) if key_room_candidates.size() > 0: key_room = key_room_candidates[rng.randi() % key_room_candidates.size()] else: # Fallback: use start room key_room = start_room else: # Fallback: use start room key_room = start_room var door_data = { "type": "KeyDoor", "direction": direction, "position": Vector2(door_world_x, door_world_y), "tile_x": middle_tile_x, "tile_y": middle_tile_y, "door": door, "blocking_room": blocking_room, "is_closed": true, # KeyDoors always closed "key_room": key_room # Room where key is placed (before this door) } blocking_doors.append(door_data) return { "doors": blocking_doors, "puzzle_data": room_puzzle_data } func _find_floor_switch_position(room: Dictionary, grid: Array, map_size: Vector2i, rng: RandomNumberGenerator, exclude_door_x: int = -1, exclude_door_y: int = -1) -> Dictionary: # Find a valid floor position for a floor switch in the room # exclude_door_x, exclude_door_y: Tile coordinates of door to avoid placing switch too close to # Returns a dictionary with position (Vector2) and tile_x, tile_y (int) or empty dict if no position found var tile_size = 16 var valid_positions = [] var min_distance_from_door = 3 # Minimum tiles away from door # Room interior is from room.x + 2 to room.x + room.w - 2 (excluding 2-tile walls) # Also exclude door tiles (grid value 2) to avoid placing switches in doorways 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 (not a door tile) if grid[x][y] == 1: # Floor (not door which is 2) # Check if position is too close to door (if door position provided) var too_close_to_door = false if exclude_door_x >= 0 and exclude_door_y >= 0: var distance_x = abs(x - exclude_door_x) var distance_y = abs(y - exclude_door_y) var distance = max(distance_x, distance_y) # Chebyshev distance (tiles) if distance < min_distance_from_door: too_close_to_door = true if not too_close_to_door: var world_x = x * tile_size + tile_size / 2.0 var world_y = y * tile_size + tile_size / 2.0 valid_positions.append({ "position": Vector2(world_x, world_y), "tile_x": x, "tile_y": y }) if valid_positions.size() > 0: # Pick a random position return valid_positions[rng.randi() % valid_positions.size()] return {} func _determine_door_direction(door: Dictionary, _all_rooms: Array) -> String: # Determine door direction based on door position and connected rooms # Door on upper wall = "Up", left wall = "Left", etc. # This returns direction relative to room1 if not "room1" in door or not "room2" in door: return "Up" # Default var room1 = door.room1 var room2 = door.room2 # Determine which wall the door is on by comparing room positions # If room2 is above room1, door is on top wall of room1 (Up) # If room2 is below room1, door is on bottom wall of room1 (Down) # If room2 is left of room1, door is on left wall of room1 (Left) # If room2 is right of room1, door is on right wall of room1 (Right) var dx = room2.x - room1.x var dy = room2.y - room1.y # Check which direction has the larger difference if abs(dy) > abs(dx): # Vertical alignment if dy < 0: return "Up" # room2 is above room1 - door on top wall of room1 else: return "Down" # room2 is below room1 - door on bottom wall of room1 else: # Horizontal alignment if dx < 0: return "Left" # room2 is left of room1 - door on left wall of room1 else: return "Right" # room2 is right of room1 - door on right wall of room1 func _determine_door_direction_for_puzzle_room(door: Dictionary, puzzle_room: Dictionary, _all_rooms: Array) -> String: # Determine which WALL of the PUZZLE ROOM the door is on # CRITICAL: door.x and door.y are the position in room1, not necessarily in the puzzle room # Need to check which room is the puzzle room and determine the wall based on door direction var door_room1 = door.room1 if ("room1" in door and door.room1 and not door.room1.is_empty()) else null var door_room2 = door.room2 if ("room2" in door and door.room2 and not door.room2.is_empty()) else null # Check which room is the puzzle room var puzzle_is_room1 = false var puzzle_is_room2 = false if door_room1: puzzle_is_room1 = (door_room1.x == puzzle_room.x and door_room1.y == puzzle_room.y and \ door_room1.w == puzzle_room.w and door_room1.h == puzzle_room.h) if door_room2: puzzle_is_room2 = (door_room2.x == puzzle_room.x and door_room2.y == puzzle_room.y and \ door_room2.w == puzzle_room.w and door_room2.h == puzzle_room.h) # Get door direction from door.dir if "dir" in door: var door_dir = door.dir if puzzle_is_room1: # Puzzle room is room1 - door.dir represents the wall of puzzle room directly match door_dir: "E": return "Right" # Door on right wall of puzzle room "W": return "Left" # Door on left wall of puzzle room "N": return "Up" # Door on top wall of puzzle room "S": return "Down" # Door on bottom wall of puzzle room elif puzzle_is_room2: # Puzzle room is room2 - door.dir is FROM room1, so flip it match door_dir: "E": return "Left" # Door on left wall of puzzle room (room2 is to the right of room1) "W": return "Right" # Door on right wall of puzzle room (room2 is to the left of room1) "N": return "Down" # Door on bottom wall of puzzle room (room2 is below room1) "S": return "Up" # Door on top wall of puzzle room (room2 is above room1) # Fallback: calculate based on door position relative to puzzle room center var door_x = door.x var door_y = door.y var puzzle_center_x = puzzle_room.x + puzzle_room.w / 2.0 var puzzle_center_y = puzzle_room.y + puzzle_room.h / 2.0 var dx = door_x - puzzle_center_x var dy = door_y - puzzle_center_y # Determine which wall based on which direction from center if abs(dy) > abs(dx): # Vertical - door is more above/below than left/right if dy < 0: return "Up" # Door is above puzzle room center - door is on top wall else: return "Down" # Door is below puzzle room center - door is on bottom wall else: # Horizontal - door is more left/right than above/below if dx < 0: return "Left" # Door is left of puzzle room center - door is on left wall else: return "Right" # Door is right of puzzle room center - door is on right wall