2026-03-16 16:24:32 +11:00
|
|
|
## MapGenerator.gd
|
|
|
|
|
# Responsible for procedural generation of dungeon maps, including rooms, corridors, and walls.
|
|
|
|
|
class_name MapGenerator
|
|
|
|
|
extends RefCounted
|
|
|
|
|
|
|
|
|
|
# Main entry point: generates a new MapData resource based on the provided settings.
|
|
|
|
|
# Handles room placement, corridor carving, wall painting, and spawn point selection.
|
|
|
|
|
func generate(settings: MapSettings) -> MapData:
|
|
|
|
|
var map_data: MapData = MapData.new()
|
|
|
|
|
# Randomly choose how many rooms to generate within the allowed range.
|
|
|
|
|
map_data.room_count = randi_range(settings.min_room_count, settings.max_room_count)
|
|
|
|
|
|
|
|
|
|
_generate_rooms(map_data, settings)
|
|
|
|
|
_generate_corridors(map_data, settings)
|
|
|
|
|
_generate_walls(map_data)
|
|
|
|
|
_pick_spawn_point(map_data)
|
|
|
|
|
|
|
|
|
|
return map_data
|
|
|
|
|
|
|
|
|
|
# Places non-overlapping rectangular rooms in the map grid.
|
|
|
|
|
# Each room is checked for overlap and only added if it fits.
|
|
|
|
|
func _generate_rooms(data: MapData, settings: MapSettings) -> void:
|
|
|
|
|
for _room_idx in range(data.room_count):
|
|
|
|
|
for _try in range(settings.max_tries):
|
|
|
|
|
var w: int = randi_range(settings.min_room_size, settings.max_room_size)
|
|
|
|
|
var h: int = randi_range(settings.min_room_size, settings.max_room_size)
|
|
|
|
|
var x: int = randi_range(1, settings.width - w - 1)
|
|
|
|
|
var y: int = randi_range(1, settings.height - h - 1)
|
|
|
|
|
|
|
|
|
|
var new_room: Rect2i = Rect2i(x, y, w, h)
|
|
|
|
|
|
|
|
|
|
var overlaps: bool = false
|
|
|
|
|
for existing_room in data.rooms:
|
|
|
|
|
if not (new_room.position.x + new_room.size.x + settings.room_padding <= existing_room.position.x or
|
|
|
|
|
new_room.position.x >= existing_room.position.x + existing_room.size.x + settings.room_padding or
|
|
|
|
|
new_room.position.y + new_room.size.y + settings.room_padding <= existing_room.position.y or
|
|
|
|
|
new_room.position.y >= existing_room.position.y + existing_room.size.y + settings.room_padding):
|
|
|
|
|
overlaps = true
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
if not overlaps:
|
|
|
|
|
data.rooms.append(new_room)
|
|
|
|
|
for rx in range(new_room.position.x, new_room.end.x):
|
|
|
|
|
for ry in range(new_room.position.y, new_room.end.y):
|
|
|
|
|
data.grid[Vector2i(rx, ry)] = MapData.TileType.FLOOR
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
# Connects all rooms with corridors using Manhattan paths (L-shaped).
|
|
|
|
|
# Also adds a few extra loops for map variety.
|
|
|
|
|
func _generate_corridors(data: MapData, _settings: MapSettings) -> void:
|
|
|
|
|
var room_centers: Array[Vector2i] = []
|
|
|
|
|
for room in data.rooms:
|
|
|
|
|
room_centers.append(room.get_center())
|
|
|
|
|
|
|
|
|
|
if room_centers.is_empty(): return
|
|
|
|
|
|
|
|
|
|
var connected: Array[Vector2i] = []
|
|
|
|
|
var unconnected: Array[Vector2i] = room_centers.duplicate()
|
|
|
|
|
var potential_loops: Array[Array] = []
|
|
|
|
|
|
|
|
|
|
connected.append(unconnected.pop_front())
|
|
|
|
|
|
|
|
|
|
while unconnected.size() > 0:
|
|
|
|
|
var best_distance: int = 999999
|
|
|
|
|
var best_pair: Array[Vector2i] = [Vector2i.ZERO, Vector2i.ZERO]
|
|
|
|
|
var best_un_idx: int = -1
|
|
|
|
|
|
|
|
|
|
for i in range(connected.size()):
|
|
|
|
|
for j in range(unconnected.size()):
|
|
|
|
|
var p1: Vector2i = connected[i]
|
|
|
|
|
var p2: Vector2i = unconnected[j]
|
|
|
|
|
var dist: int = abs(p1.x - p2.x) + abs(p1.y - p2.y)
|
|
|
|
|
if dist < best_distance:
|
|
|
|
|
best_distance = dist
|
|
|
|
|
best_pair = [p1, p2]
|
|
|
|
|
best_un_idx = j
|
|
|
|
|
else:
|
|
|
|
|
potential_loops.append([p1, p2])
|
|
|
|
|
|
|
|
|
|
_carve_manhattan_corridor(data, best_pair[0], best_pair[1])
|
|
|
|
|
connected.append(unconnected[best_un_idx])
|
|
|
|
|
unconnected.remove_at(best_un_idx)
|
|
|
|
|
|
|
|
|
|
# Add extra loops (~15% of room count) for more interesting layouts
|
|
|
|
|
potential_loops.shuffle()
|
|
|
|
|
var loop_count: int = int(data.rooms.size() * 0.15)
|
|
|
|
|
for i in range(min(loop_count, potential_loops.size())):
|
|
|
|
|
_carve_manhattan_corridor(data, potential_loops[i][0], potential_loops[i][1])
|
|
|
|
|
|
|
|
|
|
# Helper to carve a corridor between two points using Manhattan (L-shaped) path.
|
|
|
|
|
# Chooses the best valid path and writes it to the grid.
|
|
|
|
|
func _carve_manhattan_corridor(data: MapData, start: Vector2i, end: Vector2i) -> void:
|
|
|
|
|
var path_hv: Array[Vector2i] = _get_manhattan_points(start, end, false)
|
|
|
|
|
var path_vh: Array[Vector2i] = _get_manhattan_points(start, end, true)
|
|
|
|
|
|
|
|
|
|
# Determine which room indices we are connecting for validation
|
|
|
|
|
var ri_a: int = -1
|
|
|
|
|
var ri_b: int = -1
|
|
|
|
|
for i in range(data.rooms.size()):
|
|
|
|
|
if data.rooms[i].has_point(start): ri_a = i
|
|
|
|
|
if data.rooms[i].has_point(end): ri_b = i
|
|
|
|
|
|
|
|
|
|
var chosen: Array[Vector2i] = path_hv
|
|
|
|
|
if _is_corridor_valid(data, path_hv, ri_a, ri_b):
|
|
|
|
|
chosen = path_hv
|
|
|
|
|
elif _is_corridor_valid(data, path_vh, ri_a, ri_b):
|
|
|
|
|
chosen = path_vh
|
|
|
|
|
|
|
|
|
|
for pt in chosen:
|
|
|
|
|
data.grid[pt] = MapData.TileType.FLOOR
|
|
|
|
|
|
|
|
|
|
# Returns an array of points representing a Manhattan (L-shaped) path between two points.
|
|
|
|
|
# v_first determines whether to go vertical then horizontal, or vice versa.
|
|
|
|
|
func _get_manhattan_points(start: Vector2i, end: Vector2i, v_first: bool) -> Array[Vector2i]:
|
|
|
|
|
var pts: Array[Vector2i] = []
|
|
|
|
|
var x_step: int = sign(end.x - start.x) if end.x != start.x else 1
|
|
|
|
|
var y_step: int = sign(end.y - start.y) if end.y != start.y else 1
|
2026-03-16 16:46:21 +11:00
|
|
|
var min_len: int = 2 # Minimum segment length before changing direction
|
2026-03-16 16:24:32 +11:00
|
|
|
|
|
|
|
|
if v_first:
|
2026-03-16 16:46:21 +11:00
|
|
|
var y_len: int = abs(end.y - start.y)
|
|
|
|
|
var y_end: int = start.y
|
|
|
|
|
if y_len < min_len:
|
|
|
|
|
y_end = start.y + y_step * min_len
|
|
|
|
|
# Clamp to end.y if overshoot
|
|
|
|
|
if (y_step > 0 and y_end > end.y) or (y_step < 0 and y_end < end.y):
|
|
|
|
|
y_end = end.y
|
|
|
|
|
else:
|
|
|
|
|
y_end = end.y
|
|
|
|
|
for y in range(start.y, y_end + y_step, y_step):
|
2026-03-16 16:24:32 +11:00
|
|
|
pts.append(Vector2i(start.x, y))
|
|
|
|
|
for x in range(start.x + x_step, end.x + x_step, x_step):
|
2026-03-16 16:46:21 +11:00
|
|
|
pts.append(Vector2i(x, y_end))
|
|
|
|
|
# If y_end != end.y, finish remaining y segment
|
|
|
|
|
if y_end != end.y:
|
|
|
|
|
for y in range(y_end + y_step, end.y + y_step, y_step):
|
|
|
|
|
pts.append(Vector2i(end.x, y))
|
2026-03-16 16:24:32 +11:00
|
|
|
else:
|
2026-03-16 16:46:21 +11:00
|
|
|
var x_len: int = abs(end.x - start.x)
|
|
|
|
|
var x_end: int = start.x
|
|
|
|
|
if x_len < min_len:
|
|
|
|
|
x_end = start.x + x_step * min_len
|
|
|
|
|
if (x_step > 0 and x_end > end.x) or (x_step < 0 and x_end < end.x):
|
|
|
|
|
x_end = end.x
|
|
|
|
|
else:
|
|
|
|
|
x_end = end.x
|
|
|
|
|
for x in range(start.x, x_end + x_step, x_step):
|
2026-03-16 16:24:32 +11:00
|
|
|
pts.append(Vector2i(x, start.y))
|
|
|
|
|
for y in range(start.y + y_step, end.y + y_step, y_step):
|
2026-03-16 16:46:21 +11:00
|
|
|
pts.append(Vector2i(x_end, y))
|
|
|
|
|
if x_end != end.x:
|
|
|
|
|
for x in range(x_end + x_step, end.x + x_step, x_step):
|
|
|
|
|
pts.append(Vector2i(x, end.y))
|
2026-03-16 16:24:32 +11:00
|
|
|
return pts
|
|
|
|
|
|
|
|
|
|
# Returns true if the corridor path does not violate the 2-tile gap rule between rooms.
|
|
|
|
|
# Ensures corridors do not pass too close to other rooms.
|
|
|
|
|
func _is_corridor_valid(data: MapData, points: Array[Vector2i], ri_a: int, ri_b: int) -> bool:
|
|
|
|
|
for pt in points:
|
|
|
|
|
var inside_any_room: bool = false
|
|
|
|
|
for r in data.rooms:
|
|
|
|
|
if r.has_point(pt):
|
|
|
|
|
inside_any_room = true
|
|
|
|
|
break
|
|
|
|
|
if inside_any_room: continue
|
|
|
|
|
|
|
|
|
|
# Check gap from other rooms
|
|
|
|
|
for i in range(data.rooms.size()):
|
|
|
|
|
if i == ri_a or i == ri_b: continue
|
|
|
|
|
if data.rooms[i].grow(2).has_point(pt):
|
|
|
|
|
return false
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
# Paints wall tiles around all floor tiles in the grid.
|
|
|
|
|
# Any empty neighbor of a floor tile becomes a wall.
|
|
|
|
|
func _generate_walls(data: MapData) -> void:
|
|
|
|
|
for tile: Vector2i in data.grid.keys():
|
|
|
|
|
if data.grid[tile] == MapData.TileType.FLOOR:
|
|
|
|
|
for dx in range(-1, 2):
|
|
|
|
|
for dy in range(-1, 2):
|
|
|
|
|
var neighbor: Vector2i = tile + Vector2i(dx, dy)
|
|
|
|
|
if not data.grid.has(neighbor):
|
|
|
|
|
data.grid[neighbor] = MapData.TileType.WALL
|
|
|
|
|
|
|
|
|
|
# Picks a spawn point for the player (center of the first room).
|
|
|
|
|
func _pick_spawn_point(data: MapData) -> void:
|
|
|
|
|
if not data.rooms.is_empty():
|
|
|
|
|
data.spawn_point = data.rooms[0].get_center()
|