## 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 if v_first: for y in range(start.y, end.y + y_step, y_step): pts.append(Vector2i(start.x, y)) for x in range(start.x + x_step, end.x + x_step, x_step): pts.append(Vector2i(x, end.y)) else: for x in range(start.x, end.x + x_step, x_step): pts.append(Vector2i(x, start.y)) for y in range(start.y + y_step, end.y + y_step, y_step): pts.append(Vector2i(end.x, y)) 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()