#=============================================================================== # 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 } })