Files
infinitefusion-e18/Data/Scripts/012_Overworld/008_Overworld_RandomDungeons.rb
2022-10-01 18:06:15 +01:00

1091 lines
44 KiB
Ruby

#===============================================================================
# Code that generates a random dungeon layout, and implements it in a given map.
#===============================================================================
module RandomDungeon
#=============================================================================
# Bitwise values used to keep track of the generation of node connections.
#=============================================================================
module EdgeMasks
NORTH = 1
EAST = 2
SOUTH = 4
WEST = 8
end
#=============================================================================
# A node in a randomly generated dungeon. There is one node per cell, and
# nodes are connected to each other.
#=============================================================================
class MazeNode
def initialize
@visitable = false
@visited = false
@room = false
block_all_edges # A bit being 1 means its edge is NOT connected to the adjacent node
end
def edge_pattern; return @edges; end
def block_edge(e); @edges |= e; end
def connect_edge(e); @edges &= ~e; end
def block_all_edges; @edges = 15; end
def connect_all_edges; @edges = 0; end
def edge_blocked?(e); return (@edges & e) != 0; end
def all_edges_blocked?; return @edges != 0; end
def visitable?; return @visitable; end
def set_visitable; @visitable = true; end
def visited?; return @visited; end
def set_visited; @visited = true; end
def room?; return @room; end
def set_room; @room = true; end
end
#=============================================================================
# Maze generator. Given the number of nodes horizontally and vertically in a
# map, connects all the nodes together.
#=============================================================================
class Maze
attr_accessor :node_count_x, :node_count_y
DIRECTIONS = [EdgeMasks::NORTH, EdgeMasks::SOUTH, EdgeMasks::EAST, EdgeMasks::WEST]
def initialize(cw, ch, parameters)
raise ArgumentError.new if cw == 0 || ch == 0
@node_count_x = cw
@node_count_y = ch
@parameters = parameters
@nodes = Array.new(@node_count_x * @node_count_y) { MazeNode.new }
end
def valid_node?(x, y)
return x >= 0 && x < @node_count_x && y >= 0 && y < @node_count_y
end
def get_node(x, y)
return @nodes[(y * @node_count_x) + x] if valid_node?(x, y)
return nil
end
def node_visited?(x, y)
return true if !valid_node?(x, y) || !@nodes[(y * @node_count_x) + x].visitable?
return @nodes[(y * @node_count_x) + x].visited?
end
def set_node_visited(x, y)
@nodes[(y * @node_count_x) + x].set_visited if valid_node?(x, y)
end
def node_edge_blocked?(x, y, edge)
return false if !valid_node?(x, y)
return @nodes[(y * @node_count_x) + x].edge_blocked?(edge)
end
def connect_node_edges(x, y, edge)
return if !valid_node?(x, y)
@nodes[(y * @node_count_x) + x].connect_edge(edge)
new_x, new_y, new_edge = get_coords_in_direction(x, y, edge, true)
raise ArgumentError.new if new_edge == 0
@nodes[(new_y * @node_count_x) + new_x].connect_edge(new_edge) if valid_node?(new_x, new_y)
end
def room_count
ret = 0
@nodes.each { |node| ret += 1 if node.room? }
return ret
end
def get_coords_in_direction(x, y, dir, include_direction = false)
new_x = x
new_y = y
new_dir = 0
case dir
when EdgeMasks::NORTH
new_dir = EdgeMasks::SOUTH
new_y -= 1
when EdgeMasks::SOUTH
new_dir = EdgeMasks::NORTH
new_y += 1
when EdgeMasks::WEST
new_dir = EdgeMasks::EAST
new_x -= 1
when EdgeMasks::EAST
new_dir = EdgeMasks::WEST
new_x += 1
end
return new_x, new_y, new_dir if include_direction
return new_x, new_y
end
#===========================================================================
def generate_layout
# Set visitable nodes
visitable_nodes = set_visitable_nodes
# Generate connections between all nodes
generate_depth_first_maze(visitable_nodes)
add_more_connections
# Spawn rooms in some nodes
spawn_rooms(visitable_nodes)
end
# Returns whether the node at (x, y) is active in the given layout.
def check_active_node(x, y, layout)
case layout
when :no_corners
return false if [0, @node_count_x - 1].include?(x) && [0, @node_count_y - 1].include?(y)
when :ring
return false if x > 0 && x < @node_count_x - 1 && y > 0 && y < @node_count_y - 1
when :antiring
return false if x == 0 || x == @node_count_x - 1 || y == 0 || y == @node_count_y - 1
when :plus
return false if x != @node_count_x / 2 && y != @node_count_y / 2
when :diagonal_up
return false if (x + y - @node_count_y + 1).abs >= 2
when :diagonal_down
return false if (x - y).abs >= 2
when :cross
return false if (x - y).abs >= 2 && (x + y - @node_count_y + 1).abs >= 2
when :quadrants
return false if (x == 0 || x == @node_count_x - 1) && y >= 2 && y < @node_count_y - 2
return false if (y == 0 || y == @node_count_y - 1) && x >= 2 && x < @node_count_x - 2
end
return true
end
def set_visitable_nodes
visitable_nodes = []
@node_count_y.times do |y|
@node_count_x.times do |x|
next if !check_active_node(x, y, @parameters.node_layout)
@nodes[(y * @node_count_x) + x].set_visitable
visitable_nodes.push([x, y])
end
end
return visitable_nodes
end
def generate_depth_first_maze(visitable_nodes)
# Pick a cell to start in
start = visitable_nodes.sample
sx = start[0]
sy = start[1]
# Generate a maze
connect_nodes_and_recurse_depth_first(sx, sy, 0)
end
def connect_nodes_and_recurse_depth_first(x, y, depth)
set_node_visited(x, y)
dirs = DIRECTIONS.shuffle
4.times do |c|
dir = dirs[c]
cx, cy = get_coords_in_direction(x, y, dir)
next if node_visited?(cx, cy)
connect_node_edges(x, y, dir)
connect_nodes_and_recurse_depth_first(cx, cy, depth + 1)
end
end
def add_more_connections
return if @parameters.extra_connections_count == 0
possible_conns = []
@node_count_x.times do |x|
@node_count_y.times do |y|
node = @nodes[(y * @node_count_x) + x]
next if !node.visitable?
DIRECTIONS.each do |dir|
next if !node.edge_blocked?(dir)
cx, cy, cdir = get_coords_in_direction(x, y, dir, true)
new_node = get_node(cx, cy)
next if !new_node || !new_node.visitable? || !new_node.edge_blocked?(cdir)
possible_conns.push([x, y, dir])
end
end
end
possible_conns.sample(@parameters.extra_connections_count).each do |conn|
connect_node_edges(*conn)
end
end
def spawn_rooms(visitable_nodes)
roomable_nodes = []
visitable_nodes.each { |coord| roomable_nodes.push(coord) if check_active_node(*coord, @parameters.room_layout) }
room_count = [roomable_nodes.length * @parameters.room_chance / 100, 1].max
return if room_count == 0
rooms = roomable_nodes.sample(room_count)
rooms.each { |coords| @nodes[(coords[1] * @node_count_x) + coords[0]].set_room }
end
end
#=============================================================================
# Arrays of tile types in the dungeon map.
#=============================================================================
class DungeonLayout
attr_accessor :width, :height
alias xsize width
alias ysize height
# Used for debugging when printing out an ASCII image of the dungeon
TEXT_SYMBOLS = {
:void => "#",
:room => " ",
:corridor => " ",
:void_decoration => "#",
:void_decoration_large => "#",
:floor_decoration => " ",
:floor_decoration_large => " ",
:floor_patch => " ",
:wall_top => " ",
:wall_1 => Console.markup_style("=", bg: :brown),
:wall_2 => Console.markup_style("=", bg: :brown),
:wall_3 => Console.markup_style("=", bg: :brown),
:wall_4 => Console.markup_style("=", bg: :brown),
:wall_6 => Console.markup_style("=", bg: :brown),
:wall_7 => Console.markup_style("=", bg: :brown),
:wall_8 => Console.markup_style("=", bg: :brown),
:wall_9 => Console.markup_style("=", bg: :brown),
:wall_in_1 => Console.markup_style("=", bg: :brown),
:wall_in_3 => Console.markup_style("=", bg: :brown),
:wall_in_7 => Console.markup_style("=", bg: :brown),
:wall_in_9 => Console.markup_style("=", bg: :brown),
:upper_wall_1 => Console.markup_style("~", bg: :gray),
:upper_wall_2 => Console.markup_style("~", bg: :gray),
:upper_wall_3 => Console.markup_style("~", bg: :gray),
:upper_wall_4 => Console.markup_style("~", bg: :gray),
:upper_wall_6 => Console.markup_style("~", bg: :gray),
:upper_wall_7 => Console.markup_style("~", bg: :gray),
:upper_wall_8 => Console.markup_style("~", bg: :gray),
:upper_wall_9 => Console.markup_style("~", bg: :gray),
:upper_wall_in_1 => Console.markup_style("~", bg: :gray),
:upper_wall_in_3 => Console.markup_style("~", bg: :gray),
:upper_wall_in_7 => Console.markup_style("~", bg: :gray),
:upper_wall_in_9 => Console.markup_style("~", bg: :gray),
}
def initialize(width, height)
@width = width
@height = height
@array = [[], [], []]
clear
end
def [](x, y, layer)
return @array[layer][(y * @width) + x]
end
def []=(x, y, layer, value)
@array[layer][(y * @width) + x] = value
end
def value(x, y)
return :void if x < 0 || x >= @width || y < 0 || y >= @height
ret = :void
[2, 1, 0].each do |layer|
return @array[layer][(y * @width) + x] if @array[layer][(y * @width) + x] != :none
end
return ret
end
def clear
@array.each_with_index do |arr, layer|
(@width * @height).times { |i| arr[i] = (layer == 0) ? :void : :none }
end
end
def set_wall(x, y, value)
@array[0][(y * @width) + x] = :room
@array[1][(y * @width) + x] = value
end
def set_ground(x, y, value)
@array[0][(y * @width) + x] = value
@array[1][(y * @width) + x] = :none
end
def write
ret = ""
@height.times do |y|
@width.times do |x|
ret += TEXT_SYMBOLS[value(x, y)] || "\e[30m\e[41m?\e[0m"
end
ret += "\r\n"
end
return ret
end
end
#=============================================================================
# The main dungeon generator class.
#=============================================================================
class Dungeon
attr_accessor :width, :height
alias xsize width
alias ysize height
attr_accessor :parameters, :rng_seed
attr_accessor :tileset
# 0 is none (index 0 only) or corridor/floor
# -1 are tile combinations that need special attention
# Other numbers correspond to tile types (see def get_wall_tile_for_coord)
FLOOR_NEIGHBOURS_TO_WALL = [
0, 2, 1, 2, 4, 11, 4, 11, 7, 9, 4, 11, 4, 11, 4, 11,
8, 0, 17, 0, 17, 0, 17, 0, 8, 0, 17, 0, 17, 0, 17, 0,
9, 13, -1, 13, 17, 0, 17, 0, 8, 0, 17, 0, 17, 0, 17, 0,
8, 0, 17, 0, 17, 0, 17, 0, 8, 0, 17, 0, 17, 0, 17, 0,
6, 13, 13, 13, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0,
19, 0, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0,
6, 13, 13, 13, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0,
19, 0, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0,
3, 2, 2, 2, 11, 11, 11, 11, -1, 11, 11, 11, 11, 11, 11, 11,
19, 0, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0,
6, 13, 13, 13, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0,
19, 0, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0,
6, 13, 13, 13, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0,
19, 0, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0,
6, 13, 13, 13, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0,
19, 0, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0
]
def initialize(width, height, tileset, parameters = nil)
@tileset = tileset
@buffer_x = ((Graphics.width.to_f / Game_Map::TILE_WIDTH) / 2).ceil
@buffer_y = ((Graphics.height.to_f / Game_Map::TILE_HEIGHT) / 2).ceil
if @tileset.snap_to_large_grid
@buffer_x += 1 if @buffer_x.odd?
@buffer_y += 1 if @buffer_x.odd?
end
@parameters = parameters || GameData::DungeonParameters.new({})
if @tileset.snap_to_large_grid
@parameters.cell_width -= 1 if @parameters.cell_width.odd?
@parameters.cell_height -= 1 if @parameters.cell_height.odd?
@parameters.corridor_width += (@parameters.corridor_width == 1) ? 1 : -1 if @parameters.corridor_width.odd?
end
if width >= 20
@width = width
else
@width = (width * @parameters.cell_width) + (2 * @buffer_x)
@width += 1 if @tileset.snap_to_large_grid && @width.odd?
end
if height >= 20
@height = height
else
@height = (height * @parameters.cell_height) + (2 * @buffer_y)
@height += 1 if @tileset.snap_to_large_grid && @height.odd?
end
@usable_width = @width
@usable_height = @height
if @tileset.snap_to_large_grid
@usable_width -= 1 if @usable_width.odd?
@usable_height -= 1 if @usable_height.odd?
end
@map_data = DungeonLayout.new(@width, @height)
@need_redraw = false
end
def [](x, y, layer = nil)
return @map_data.value(x, y) if layer.nil?
return @map_data[x, y, layer]
end
def []=(x, y, layer, value)
@map_data[x, y, layer] = value
end
def write
return @map_data.write
end
#===========================================================================
# Returns whether the given coordinates are a room floor that isn't too
# close to a corridor. For positioning events/the player upon entering.
def isRoom?(x, y)
return false if @map_data.value(x, y) != :room
(-1..1).each do |i|
(-1..1).each do |j|
next if i == 0 && j == 0
return false if @map_data.value(x + i, y + j) == :corridor
end
end
return true # No surrounding tiles are corridor floor
end
def tile_is_ground?(value)
return [:room, :corridor].include?(value)
end
# Lower wall tiles only.
def tile_is_wall?(value)
return [:wall_1, :wall_2, :wall_3, :wall_4, :wall_6, :wall_7, :wall_8, :wall_9,
:wall_in_1, :wall_in_3, :wall_in_7, :wall_in_9].include?(value)
end
def coord_is_ground?(x, y)
return tile_is_ground?(@map_data[x, y, 0]) && !tile_is_wall?(@map_data[x, y, 1])
end
#===========================================================================
def generate
@rng_seed = @parameters.rng_seed || Random.new_seed
Random.srand(@rng_seed)
maxWidth = @usable_width - (@buffer_x * 2)
maxHeight = @usable_height - (@buffer_y * 2)
return if maxWidth < 0 || maxHeight < 0
loop do
@need_redraw = false
@map_data.clear
# Generate the basic layout of the map
generate_layout(maxWidth, maxHeight)
next if @need_redraw
# Draw walls
generate_walls(maxWidth, maxHeight)
next if @need_redraw
# Draw decorations
paint_decorations(maxWidth, maxHeight)
# Draw wall top tiles
paint_wall_top_tiles(maxWidth, maxHeight)
break #if !@need_redraw
end
end
def generate_layout(maxWidth, maxHeight)
cellWidth = @parameters.cell_width
cellHeight = @parameters.cell_height
# Map is too small, make the whole map a room
if maxWidth < cellWidth || maxHeight < cellHeight
paint_ground_rect(@buffer_x, @buffer_y, maxWidth, maxHeight, :room)
return
end
# Generate connections between cells
maze = Maze.new(maxWidth / cellWidth, maxHeight / cellHeight, @parameters)
maze.generate_layout
# If no rooms were generated, make the whole map a room
if maze.room_count == 0
paint_ground_rect(@buffer_x, @buffer_y, maxWidth, maxHeight, :room)
return
end
# Draw each cell's contents in turn (room and corridors)
(maxHeight / cellHeight).times do |y|
(maxWidth / cellWidth).times do |x|
paint_node_contents(@buffer_x + (x * cellWidth), @buffer_y + (y * cellHeight), maze.get_node(x, y))
end
end
check_for_isolated_rooms
end
def generate_walls(maxWidth, maxHeight)
# Lower layer
errors = []
maxHeight.times do |y|
maxWidth.times do |x|
next if !coord_is_ground?(@buffer_x + x, @buffer_y + y)
paint_walls_around_ground(@buffer_x + x, @buffer_y + y, 0, errors)
end
end
# Check for error tiles
errors.each do |coord|
resolve_wall_error(coord[0], coord[1], 0)
break if @need_redraw
end
return if @need_redraw
return if !@tileset.double_walls
# Upper layer
errors = []
(maxHeight + 2).times do |y|
(maxWidth + 2).times do |x|
next if !tile_is_wall?(@map_data[@buffer_x + x - 1, @buffer_y + y - 1, 1])
paint_walls_around_ground(@buffer_x + x - 1, @buffer_y + y - 1, 1, errors)
end
end
# Check for error tiles
errors.each do |coord|
resolve_wall_error(coord[0], coord[1], 1)
break if @need_redraw
end
end
#===========================================================================
# Determines whether all floor tiles are contiguous. Sets @need_redraw if
# there are 2+ floor regions that are isolated from each other.
def check_for_isolated_rooms
# Get a floor tile as a starting position
start = nil
maxWidth = @usable_width - (@buffer_x * 2)
maxHeight = @usable_height - (@buffer_y * 2)
for y in 0...maxHeight
for x in 0...maxWidth
next if !tile_is_ground?(@map_data[x + @buffer_x, y + @buffer_y, 0])
start = [x, y]
break
end
break if start
end
if !start
@need_redraw = true
return
end
# Flood fill (https://en.wikipedia.org/wiki/Flood_fill#Span_Filling)
to_check = [
[start[0], start[0], start[1], 1],
[start[0], start[0], start[1] - 1, -1]
]
visited = []
loop do
break if to_check.empty?
checking = to_check.shift
x1, x2, y, dy = checking
x = x1
if !visited[y * maxWidth + x] && tile_is_ground?(@map_data[x + @buffer_x, y + @buffer_y, 0])
loop do
break if visited[y * maxWidth + x - 1] || !tile_is_ground?(@map_data[x - 1 + @buffer_x, y + @buffer_y, 0])
visited[y * maxWidth + x - 1] = true
x -= 1
end
end
to_check.push([x, x1 - 1, y - dy, -dy]) if x < x1
loop do
break if x1 > x2
loop do
break if visited[y * maxWidth + x1] || !tile_is_ground?(@map_data[x1 + @buffer_x, y + @buffer_y, 0])
visited[y * maxWidth + x1] = true
to_check.push([x, x1, y + dy, dy])
to_check.push([x2 + 1, x1, y - dy, -dy]) if x1 > x2
x1 += 1
end
x1 += 1
loop do
break if x1 >= x2
break if !visited[y * maxWidth + x1] && tile_is_ground?(@map_data[x1 + @buffer_x, y + @buffer_y, 0])
x1 += 1
end
x = x1
end
end
# Check for unflooded floor tiles
for y in 0...maxHeight
for x in 0...maxWidth
next if visited[y * maxWidth + x] || !tile_is_ground?(@map_data[x + @buffer_x, y + @buffer_y, 0])
@need_redraw = true
break
end
break if @need_redraw
end
end
# Fixes (most) situations where it isn't immediately obvious how to draw a
# wall around a floor area.
def resolve_wall_error(x, y, layer = 0)
if layer == 0
is_neighbour = lambda { |til| return tile_is_ground?(til) }
else
is_neighbour = lambda { |til| return tile_is_wall?(til) }
end
tile = {
:wall_1 => (layer == 0) ? :wall_1 : :upper_wall_1,
:wall_2 => (layer == 0) ? :wall_2 : :upper_wall_2,
:wall_3 => (layer == 0) ? :wall_3 : :upper_wall_3,
:wall_4 => (layer == 0) ? :wall_4 : :upper_wall_4,
:wall_6 => (layer == 0) ? :wall_6 : :upper_wall_6,
:wall_7 => (layer == 0) ? :wall_7 : :upper_wall_7,
:wall_8 => (layer == 0) ? :wall_8 : :upper_wall_8,
:wall_9 => (layer == 0) ? :wall_9 : :upper_wall_9,
:wall_in_1 => (layer == 0) ? :wall_in_1 : :upper_wall_in_1,
:wall_in_3 => (layer == 0) ? :wall_in_3 : :upper_wall_in_3,
:wall_in_7 => (layer == 0) ? :wall_in_7 : :upper_wall_in_7,
:wall_in_9 => (layer == 0) ? :wall_in_9 : :upper_wall_in_9,
:corridor => (layer == 0) ? :corridor : :void
}
neighbours = 0
neighbours |= 0x01 if is_neighbour.call(@map_data.value(x, y - 1)) # N
neighbours |= 0x02 if is_neighbour.call(@map_data.value(x + 1, y - 1)) # NE
neighbours |= 0x04 if is_neighbour.call(@map_data.value(x + 1, y)) # E
neighbours |= 0x08 if is_neighbour.call(@map_data.value(x + 1, y + 1)) # SE
neighbours |= 0x10 if is_neighbour.call(@map_data.value(x, y + 1)) # S
neighbours |= 0x20 if is_neighbour.call(@map_data.value(x - 1, y + 1)) # SW
neighbours |= 0x40 if is_neighbour.call(@map_data.value(x - 1, y)) # W
neighbours |= 0x80 if is_neighbour.call(@map_data.value(x - 1, y - 1)) # NW
case neighbours
when 34
# --f floor tile (dashes are walls)
# -o- this tile
# f-- floor tile
if @map_data.value(x - 1, y - 1) == :void
@map_data[x, y, 1] = tile[:wall_in_3]
@map_data[x - 1, y, 1] = tile[:wall_in_7]
@map_data[x, y - 1, 1] = tile[:wall_in_7]
@map_data.set_wall(x - 1, y - 1, tile[:wall_7])
elsif @map_data.value(x + 1, y + 1) == :void
@map_data[x, y, 1] = tile[:wall_in_7]
@map_data[x + 1, y, 1] = tile[:wall_in_3]
@map_data[x, y + 1, 1] = tile[:wall_in_3]
@map_data.set_wall(x + 1, y + 1, tile[:wall_3])
elsif @map_data[x, y - 1, 1] == tile[:wall_4] && @map_data[x - 1, y, 1] == tile[:wall_in_9]
@map_data[x, y, 1] = tile[:wall_in_3]
@map_data[x, y - 1, 1] = tile[:wall_in_7]
@map_data.set_ground(x - 1, y, tile[:corridor])
@map_data[x - 1, y - 1, 1] = (@map_data[x - 1, y - 1, 1] == tile[:wall_6]) ? tile[:wall_in_9] : tile[:wall_8]
elsif @map_data[x, y - 1, 1] == tile[:wall_in_1] && @map_data[x - 1, y, 1] == tile[:wall_8]
@map_data[x, y, 1] = tile[:wall_in_3]
@map_data.set_ground(x, y - 1, tile[:corridor])
@map_data[x - 1, y, 1] = tile[:wall_in_7]
@map_data[x - 1, y - 1, 1] = (@map_data[x - 1, y - 1, 1] == tile[:wall_2]) ? tile[:wall_in_1] : tile[:wall_4]
elsif @map_data[x, y - 1, 1] == tile[:wall_in_1] && @map_data[x - 1, y, 1] == tile[:wall_in_9]
@map_data[x, y, 1] = tile[:wall_in_3]
@map_data.set_ground(x, y - 1, tile[:corridor])
@map_data.set_ground(x - 1, y, tile[:corridor])
if @map_data[x - 1, y - 1, 1] == :error
@map_data[x - 1, y - 1, 1] = tile[:wall_in_7]
else
@map_data.set_ground(x - 1, y - 1, tile[:corridor])
end
elsif @map_data[x, y + 1, 1] == tile[:wall_6] && @map_data[x + 1, y, 1] == tile[:wall_in_1]
@map_data[x, y, 1] = tile[:wall_in_7]
@map_data[x, y + 1, 1] = tile[:wall_in_3]
@map_data.set_ground(x + 1, y, tile[:corridor])
@map_data[x + 1, y + 1, 1] = (@map_data[x + 1, y + 1, 1] == tile[:wall_4]) ? tile[:wall_in_1] : tile[:wall_2]
elsif @map_data[x, y + 1, 1] == tile[:wall_in_9] && @map_data[x + 1, y, 1] == tile[:wall_2]
@map_data[x, y, 1] = tile[:wall_in_7]
@map_data.set_ground(x, y + 1, tile[:corridor])
@map_data[x + 1, y, 1] = tile[:wall_in_3]
@map_data[x + 1, y + 1, 1] = (@map_data[x + 1, y + 1, 1] == tile[:wall_8]) ? tile[:wall_in_9] : tile[:wall_6]
elsif @map_data[x, y + 1, 1] == tile[:wall_in_9] && @map_data[x + 1, y, 1] == tile[:wall_in_1]
@map_data[x, y, 1] = tile[:wall_in_7]
@map_data.set_ground(x, y + 1, tile[:corridor])
@map_data.set_ground(x + 1, y, tile[:corridor])
if @map_data[x + 1, y + 1, 1] == :error
@map_data[x + 1, y + 1, 1] = tile[:wall_in_3]
else
@map_data.set_ground(x + 1, y + 1, tile[:corridor])
end
else
# Tile error can't be resolved; will redraw map
@need_redraw = true
end
when 136
# f-- floor tile (dashes are walls)
# -o- this tile
# --f floor tile
if @map_data.value(x - 1, y + 1) == :void
@map_data[x, y, 1] = tile[:wall_in_9]
@map_data[x - 1, y, 1] = tile[:wall_in_1]
@map_data[x, y + 1, 1] = tile[:wall_in_1]
@map_data.set_wall(x - 1, y + 1, tile[:wall_1])
elsif @map_data.value(x + 1, y - 1) == :void
@map_data[x, y, 1] = tile[:wall_in_1]
@map_data[x + 1, y, 1] = tile[:wall_in_9]
@map_data[x, y - 1, 1] = tile[:wall_in_9]
@map_data.set_wall(x + 1, y - 1, tile[:wall_9])
elsif @map_data[x, y - 1, 1] == tile[:wall_6] && @map_data[x + 1, y, 1] == tile[:wall_in_7]
@map_data[x, y, 1] = tile[:wall_in_1]
@map_data[x, y - 1, 1] = tile[:wall_in_9]
@map_data.set_ground(x + 1, y, tile[:corridor])
@map_data[x + 1, y - 1, 1] = (@map_data[x + 1, y - 1, 1] == tile[:wall_4]) ? tile[:wall_in_7] : tile[:wall_8]
elsif @map_data[x, y - 1, 1] == tile[:wall_in_3] && @map_data[x + 1, y, 1] == tile[:wall_8]
@map_data[x, y, 1] = tile[:wall_in_1]
@map_data.set_ground(x, y - 1, tile[:corridor])
@map_data[x + 1, y, 1] = tile[:wall_in_9]
@map_data[x + 1, y - 1, 1] = (@map_data[x + 1, y - 1, 1] == tile[:wall_2]) ? tile[:wall_in_3] : tile[:wall_6]
elsif @map_data[x, y - 1, 1] == tile[:wall_in_3] && @map_data[x + 1, y, 1] == tile[:wall_in_7]
@map_data[x, y, 1] = tile[:wall_in_1]
@map_data.set_ground(x, y - 1, tile[:corridor])
@map_data.set_ground(x + 1, y, tile[:corridor])
if @map_data[x + 1, y - 1, 1] == :error
@map_data[x + 1, y - 1, 1] = tile[:wall_in_9]
else
@map_data.set_ground(x + 1, y - 1, tile[:corridor])
end
elsif @map_data[x, y + 1, 1] == tile[:wall_4] && @map_data[x - 1, y, 1] == tile[:wall_in_3]
@map_data[x, y, 1] = tile[:wall_in_9]
@map_data[x, y + 1, 1] = tile[:wall_in_1]
@map_data.set_ground(x - 1, y, tile[:corridor])
@map_data[x - 1, y + 1, 1] = (@map_data[x - 1, y + 1, 1] == tile[:wall_6]) ? tile[:wall_in_3] : tile[:wall_2]
elsif @map_data[x, y + 1, 1] == tile[:wall_in_7] && @map_data[x - 1, y, 1] == tile[:wall_2]
@map_data[x, y, 1] = tile[:wall_in_9]
@map_data.set_ground(x, y + 1, tile[:corridor])
@map_data[x - 1, y, 1] = tile[:wall_in_1]
@map_data[x - 1, y + 1, 1] = (@map_data[x - 1, y + 1, 1] == tile[:wall_8]) ? tile[:wall_in_7] : tile[:wall_4]
elsif @map_data[x, y + 1, 1] == tile[:wall_in_7] && @map_data[x - 1, y, 1] == tile[:wall_in_3]
@map_data[x, y, 1] = tile[:wall_in_9]
@map_data.set_ground(x, y + 1, tile[:corridor])
@map_data.set_ground(x - 1, y, tile[:corridor])
if @map_data[x - 1, y + 1, 1] == :error
@map_data[x - 1, y + 1, 1] = tile[:wall_in_1]
else
@map_data.set_ground(x - 1, y + 1, tile[:corridor])
end
else
# Tile error can't be resolved; will redraw map
@need_redraw = true
end
else
@need_redraw = true
raise "can't resolve error"
end
end
#===========================================================================
# Draws a cell's contents, which is an underlying pattern based on
# tile_layout (the corridors), and possibly a room on top of that.
def paint_node_contents(cell_x, cell_y, node)
# Draw corridors connecting this room
paint_connections(cell_x, cell_y, node.edge_pattern)
# Generate a randomly placed room
paint_room(cell_x, cell_y) if node.room?
end
def paint_ground_rect(x, y, width, height, tile)
height.times do |j|
width.times do |i|
@map_data[x + i, y + j, 0] = tile
end
end
end
# Draws corridors leading from the node at (cell_x, cell_y).
def paint_connections(cell_x, cell_y, pattern)
x_offset = (@parameters.cell_width - @parameters.corridor_width) / 2
y_offset = (@parameters.cell_height - @parameters.corridor_width) / 2
if @parameters.random_corridor_shift
variance = @parameters.corridor_width
variance /= 2 if @tileset.snap_to_large_grid
if variance > 1
x_shift = rand(variance) - (variance / 2)
y_shift = rand(variance) - (variance / 2)
if @tileset.snap_to_large_grid
x_shift *= 2
y_shift *= 2
end
x_offset += x_shift
y_offset += y_shift
end
end
if @tileset.snap_to_large_grid
x_offset += 1 if x_offset.odd?
y_offset += 1 if y_offset.odd?
end
if (pattern & RandomDungeon::EdgeMasks::NORTH) == 0
paint_ground_rect(cell_x + x_offset, cell_y,
@parameters.corridor_width, y_offset + @parameters.corridor_width,
:corridor)
end
if (pattern & RandomDungeon::EdgeMasks::SOUTH) == 0
paint_ground_rect(cell_x + x_offset, cell_y + y_offset,
@parameters.corridor_width, @parameters.cell_height - y_offset,
:corridor)
end
if (pattern & RandomDungeon::EdgeMasks::EAST) == 0
paint_ground_rect(cell_x + x_offset, cell_y + y_offset,
@parameters.cell_width - x_offset, @parameters.corridor_width,
:corridor)
end
if (pattern & RandomDungeon::EdgeMasks::WEST) == 0
paint_ground_rect(cell_x, cell_y + y_offset,
x_offset + @parameters.corridor_width, @parameters.corridor_width,
:corridor)
end
end
# Draws a room at (cell_x, cell_y).
def paint_room(cell_x, cell_y)
width, height = @parameters.rand_room_size
return if width <= 0 || height <= 0
if @tileset.snap_to_large_grid
width += (width <= @parameters.cell_width / 2) ? 1 : -1 if width.odd?
height += (height <= @parameters.cell_height / 2) ? 1 : -1 if height.odd?
end
center_x, center_y = @parameters.rand_cell_center
x = cell_x + center_x - (width / 2)
y = cell_y + center_y - (height / 2)
if @tileset.snap_to_large_grid
x += 1 if x.odd?
y += 1 if y.odd?
end
x = x.clamp(@buffer_x, @usable_width - @buffer_x - width )
y = y.clamp(@buffer_y, @usable_height - @buffer_y - height )
paint_ground_rect(x, y, width, height, :room)
end
def paint_walls_around_ground(x, y, layer, errors)
(-1..1).each do |j|
(-1..1).each do |i|
next if i == 0 && j == 0
next if @map_data[x + i, y + j, 0] != :void
tile = get_wall_tile_for_coord(x + i, y + j, layer)
if [:void, :corridor].include?(tile)
@map_data[x + i, y + j, 0] = tile
else
@map_data.set_wall(x + i, y + j, tile)
end
errors.push([x + i, y + j]) if tile == :error
end
end
end
def get_wall_tile_for_coord(x, y, layer = 0)
if layer == 0
is_neighbour = lambda { |x, y| return tile_is_ground?(@map_data.value(x, y)) }
else
is_neighbour = lambda { |x, y| return tile_is_wall?(@map_data[x, y, 1]) }
end
neighbours = 0
neighbours |= 0x01 if is_neighbour.call(x, y - 1) # N
neighbours |= 0x02 if is_neighbour.call(x + 1, y - 1) # NE
neighbours |= 0x04 if is_neighbour.call(x + 1, y) # E
neighbours |= 0x08 if is_neighbour.call(x + 1, y + 1) # SE
neighbours |= 0x10 if is_neighbour.call(x, y + 1) # S
neighbours |= 0x20 if is_neighbour.call(x - 1, y + 1) # SW
neighbours |= 0x40 if is_neighbour.call(x - 1, y) # W
neighbours |= 0x80 if is_neighbour.call(x - 1, y - 1) # NW
case FLOOR_NEIGHBOURS_TO_WALL[neighbours]
when -1 then return :error # Needs special attention
when 1 then return (layer == 0) ? :wall_1 : :upper_wall_1
when 2 then return (layer == 0) ? :wall_2 : :upper_wall_2
when 3 then return (layer == 0) ? :wall_3 : :upper_wall_3
when 4 then return (layer == 0) ? :wall_4 : :upper_wall_4
when 6 then return (layer == 0) ? :wall_6 : :upper_wall_6
when 7 then return (layer == 0) ? :wall_7 : :upper_wall_7
when 8 then return (layer == 0) ? :wall_8 : :upper_wall_8
when 9 then return (layer == 0) ? :wall_9 : :upper_wall_9
when 11 then return (layer == 0) ? :wall_in_1 : :upper_wall_in_1
when 13 then return (layer == 0) ? :wall_in_3 : :upper_wall_in_3
when 17 then return (layer == 0) ? :wall_in_7 : :upper_wall_in_7
when 19 then return (layer == 0) ? :wall_in_9 : :upper_wall_in_9
end
return :void if neighbours == 0 || layer == 1
return :corridor
end
def paint_decorations(maxWidth, maxHeight)
# Large patches (grass/sandy area)
if @tileset.has_decoration?(:floor_patch)
(maxHeight / @parameters.cell_height).times do |j|
(maxWidth / @parameters.cell_width).times do |i|
next if rand(100) >= @parameters.floor_patch_chance
# Random placing of floor patch tiles
mid_x = i * @parameters.cell_width + rand(@parameters.cell_width)
mid_y = j * @parameters.cell_height + rand(@parameters.cell_height)
((mid_y - @parameters.floor_patch_radius)..(mid_y + @parameters.floor_patch_radius)).each do |y|
((mid_x - @parameters.floor_patch_radius)..(mid_x + @parameters.floor_patch_radius)).each do |x|
if @tileset.floor_patch_under_walls
next if !tile_is_ground?(@map_data[x + @buffer_x, y + @buffer_y, 0])
else
next if !tile_is_ground?(@map_data.value(x + @buffer_x, y + @buffer_y))
end
if (((mid_x - 1)..(mid_x + 1)).include?(x) && ((mid_y - 1)..(mid_y + 1)).include?(y)) ||
rand(100) < @parameters.floor_patch_chance
@map_data[x + @buffer_x, y + @buffer_y, 0] = :floor_patch
end
end
end
# Smoothing of placed floor patch tiles
((mid_y - @parameters.floor_patch_radius)..(mid_y + @parameters.floor_patch_radius)).each do |y|
((mid_x - @parameters.floor_patch_radius)..(mid_x + @parameters.floor_patch_radius)).each do |x|
if @map_data[x + @buffer_x, y + @buffer_y, 0] == :floor_patch
adj_count = 0
adj_count += 1 if @map_data[x + @buffer_x - 1, y + @buffer_y, 0] == :floor_patch
adj_count += 1 if @map_data[x + @buffer_x, y + @buffer_y - 1, 0] == :floor_patch
adj_count += 1 if @map_data[x + @buffer_x + 1, y + @buffer_y, 0] == :floor_patch
adj_count += 1 if @map_data[x + @buffer_x, y + @buffer_y + 1, 0] == :floor_patch
if adj_count == 0 || (adj_count == 1 && rand(100) < @parameters.floor_patch_smooth_rate * 2)
@map_data[x + @buffer_x, y + @buffer_y, 0] = :corridor
end
else
if @tileset.floor_patch_under_walls
next if !tile_is_ground?(@map_data[x + @buffer_x, y + @buffer_y, 0])
else
next if !tile_is_ground?(@map_data.value(x + @buffer_x, y + @buffer_y))
end
adj_count = 0
adj_count += 1 if @map_data[x + @buffer_x - 1, y + @buffer_y, 0] == :floor_patch
adj_count += 1 if @map_data[x + @buffer_x, y + @buffer_y - 1, 0] == :floor_patch
adj_count += 1 if @map_data[x + @buffer_x + 1, y + @buffer_y, 0] == :floor_patch
adj_count += 1 if @map_data[x + @buffer_x, y + @buffer_y + 1, 0] == :floor_patch
if adj_count >= 2 && rand(100) < adj_count * @parameters.floor_patch_smooth_rate
@map_data[x + @buffer_x, y + @buffer_y, 0] = :floor_patch
end
end
end
end
end
end
end
# 2x2 floor decoration (crater)
if @tileset.has_decoration?(:floor_decoration_large)
((maxWidth * maxHeight) / @parameters.floor_decoration_large_density).times do
x = rand(maxWidth)
y = rand(maxHeight)
next if @map_data.value(x + @buffer_x, y + @buffer_y) != :room ||
@map_data.value(x + @buffer_x + 1, y + @buffer_y) != :room ||
@map_data.value(x + @buffer_x, y + @buffer_y + 1) != :room ||
@map_data.value(x + @buffer_x + 1, y + @buffer_y + 1) != :room
4.times do |c|
cx = x + @buffer_x + (c % 2)
cy = y + @buffer_y + (c / 2)
@map_data[cx, cy, 0] = (c == 0) ? :floor_decoration_large : :ignore
end
end
end
# 1x1 floor decoration
if @tileset.has_decoration?(:floor_decoration)
((@usable_width * @usable_height) / @parameters.floor_decoration_density).times do
x = rand(@usable_width)
y = rand(@usable_height)
next if !coord_is_ground?(@buffer_x + x, @buffer_y + y)
@map_data[x + @buffer_x, y + @buffer_y, 0] = :floor_decoration
end
end
# 2x2 void decoration (crevice)
if @tileset.has_decoration?(:void_decoration_large)
((@width * @height) / @parameters.void_decoration_large_density).times do
x = rand(@width - 1)
y = rand(@height - 1)
next if @map_data.value(x, y) != :void ||
@map_data.value(x + 1, y) != :void ||
@map_data.value(x, y + 1) != :void ||
@map_data.value(x + 1, y + 1) != :void
4.times do |c|
cx = x + (c % 2)
cy = y + (c / 2)
@map_data[cx, cy, 0] = (c == 0) ? :void_decoration_large : :ignore
end
end
end
# 1x1 void decoration (rock)
if @tileset.has_decoration?(:void_decoration)
((@width * @height) / @parameters.void_decoration_density).times do
x = rand(@width)
y = rand(@height)
next if @map_data.value(x, y) != :void
@map_data[x, y, 0] = :void_decoration
end
end
end
def paint_wall_top_tiles(maxWidth, maxHeight)
return if !@tileset.has_decoration?(:wall_top)
maxWidth.times do |x|
maxHeight.times do |y|
next if ![:wall_2, :wall_in_1, :wall_in_3].include?(@map_data[x + @buffer_x, y + 1 + @buffer_y, 1])
@map_data[x + @buffer_x, y + @buffer_y, 2] = :wall_top
end
end
end
#===========================================================================
# Convert dungeon layout into proper map tiles from a tileset, and modifies
# the given map's data accordingly.
def generateMapInPlace(map)
map.width.times do |i|
map.height.times do |j|
3.times do |layer|
tile_type = @map_data[i, j, layer]
tile_type = :floor if [:room, :corridor].include?(tile_type)
case tile_type
when :ignore
when :none
map.data[i, j, layer] = 0
when :void_decoration_large, :floor_decoration_large
4.times do |c|
tile = @tileset.get_random_tile_of_type(tile_type, self, i, j, layer)
tile += (c % 2) + 8 * (c / 2) if tile >= 384 # Regular tile
map.data[i + (c % 2), j + (c / 2), layer] = tile
end
else
tile = @tileset.get_random_tile_of_type(tile_type, self, i, j, layer)
map.data[i, j, layer] = tile
end
end
end
end
end
# Returns a random room tile in the dungeon that isn't too close to a
# corridor (to avoid blocking a room's entrance).
def get_random_room_tile(occupied_tiles)
ar1 = AntiRandom.new(@width)
ar2 = AntiRandom.new(@height)
((occupied_tiles.length + 1) * 1000).times do
x = ar1.get
y = ar2.get
next if !isRoom?(x, y)
next if occupied_tiles.any? { |item| (item[0] - x).abs < 2 && (item[1] - y).abs < 2 }
ret = [x, y]
occupied_tiles.push(ret)
return ret
end
return nil
end
end
end
#===============================================================================
# Variables that determine which dungeon parameters to use to generate a random
# dungeon.
#===============================================================================
class PokemonGlobalMetadata
attr_writer :dungeon_area, :dungeon_version
def dungeon_area
return @dungeon_area || :none
end
def dungeon_version
return @dungeon_version || 0
end
end
#===============================================================================
# Code that generates a random dungeon layout, and implements it in a given map.
#===============================================================================
EventHandlers.add(:on_game_map_setup, :random_dungeon,
proc { |map_id, map, _tileset_data|
next if !GameData::MapMetadata.try_get(map_id)&.random_dungeon
# Generate a random dungeon
tileset_data = GameData::DungeonTileset.try_get(map.tileset_id)
params = GameData::DungeonParameters.try_get($PokemonGlobal.dungeon_area,
$PokemonGlobal.dungeon_version)
dungeon = RandomDungeon::Dungeon.new(params.cell_count_x, params.cell_count_y,
tileset_data, params)
dungeon.generate
map.width = dungeon.width
map.height = dungeon.height
map.data.resize(map.width, map.height, 3)
dungeon.generateMapInPlace(map)
occupied_tiles = []
# Reposition the player
tile = dungeon.get_random_room_tile(occupied_tiles)
if tile
$game_temp.player_new_x = tile[0]
$game_temp.player_new_y = tile[1]
end
# Reposition events
map.events.each_value do |event|
tile = dungeon.get_random_room_tile(occupied_tiles)
if tile
event.x = tile[0]
event.y = tile[1]
end
end
}
)
#===============================================================================
# TODO: Temporary debug function for testing random dungeon generation.
#===============================================================================
MenuHandlers.add(:debug_menu, :test_random_dungeon, {
"name" => _INTL("Test Random Dungeon Generation"),
"parent" => :other_menu,
"description" => _INTL("Generates a random dungeon and echoes it to the console."),
"effect" => proc {
tileset = :cave # :forest # :cave
tileset_data = GameData::DungeonTileset.try_get((tileset == :forest) ? 23 : 7)
params = GameData::DungeonParameters.try_get(tileset)
dungeon = RandomDungeon::Dungeon.new(params.cell_count_x, params.cell_count_y, tileset_data, params)
dungeon.generate
echoln dungeon.rng_seed
echoln dungeon.write
}
})