Files
DungeonsOfKharadum/src/scripts/dungeon_generator.gd
2026-01-19 23:51:57 +01:00

2728 lines
128 KiB
GDScript3

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