1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
|
------------------------------------------------------------------------------
-- hyper_paint.lua:
--
-- Functions for painting onto usage grids (for whole layouts and for
-- smaller code rooms).
--
-- TODO: Loads of this file is redundant with procedural primitives. Really
-- we just need some sensible mappings of procedurals to paint shapes and
-- it'll be a lot more flexible.
------------------------------------------------------------------------------
hyper.paint = {}
function hyper.paint.paint_grid(paint, options, usage_grid)
for i,item in ipairs(paint) do
local feature,feature_type
if item.type == "floor" then
feature_type = "floor"
elseif item.type == "wall" then
feature_type = "wall"
elseif item.type == "space" or item.type == "proc" then
feature_type = "space"
elseif item.feature ~= nil then
feature_type = item.feature
end
-- Check which shape to paint
local shape_type = "quad"
if item.shape ~= nil then shape_type = item.shape end
-- Get information about the feature
local feature,space,solid,wall = hyper.paint.feature_flags(feature_type,options)
local feature_usage = item.usage
-- These three flags are in addition to the feature and affect how the builder treats the squares during
-- placement and subsequently.
local open = (item.open ~= nil and item.open) -- Further rooms can be placed in this area. Be careful (for now) to leave a border around this so as not to block connections
local exit = (item.exit ~= nil and item.exit) -- Same as using @ in vaults, allows this square to have connections adjacent, blocks any non-exit squares from being used as such
if item.corner1 == nil then item.corner1 = { x = 0, y = 0 } end
if item.corner2 == nil then item.corner2 = { x = usage_grid.width-1, y = usage_grid.height-1 } end
-- Paint features onto grid
-- PERF: Can slightly optimise this loop (which could be important since it gets called a lot)
-- by deciding which function we're going to use first rather than have this unwieldy if statement
if shape_type == "quad" or shape_type == "ellipse" or shape_type == "trapese" or item.type == "proc" then
-- Set layout details in the painted area
for x = item.corner1.x, item.corner2.x, 1 do
for y = item.corner1.y, item.corner2.y, 1 do
if item.type == "proc" or shape_type == "quad"
or (shape_type == "ellipse" and hyper.paint.inside_oval(x,y,item))
or (shape_type == "trapese" and hyper.paint.inside_trapese(x,y,item))
or (type(shape_type) == "function" and hyper.paint.inside_custom(x,y,item)) then
-- For procedural painting we get the feature cell-by-cell
if item.type == "proc" then
local mx,my = hyper.paint.map_to_unit(x,y,item)
-- TODO: Allow proc paint to return an additional parameter containing other properties e.g. tile, monsters, clouds, items, stuff...
-- (Although often we just want to return generic floor shapes and then tile and populate them later)
feature_type,feature_usage = item.callback(x,y,mx,my)
if feature_type ~= nil then
feature,space,solid,wall = hyper.paint.feature_flags(feature_type,options)
else
feature = nil
end
end
-- Work out where to actually paint
if feature ~= nil then
local ax,ay = x,y
if item.wrap then
ax = x % usage_grid.width
ay = y % usage_grid.height
end
local usage = { solid = solid, feature = feature, space = space, open = open, wall = wall }
if feature_usage ~= nil then
hyper.merge_options(usage,feature_usage)
end
hyper.usage.set_usage(usage_grid,ax,ay, usage)
end
end
end
end
elseif shape_type == "plot" then
if item.points ~= nil then
for i,pos in ipairs(item.points) do
hyper.usage.set_usage(layout_grid, pos.x, pos.y,{ solid = solid, feature = feature, space = space, open = open, wall = wall })
end
else
hyper.usage.set_usage(layout_grid, item.x, item.y, { solid = solid, feature = feature, space = space, open = open, wall = wall })
end
end
end
end
function hyper.paint.feature_flags(feature_type,options)
local feature
if feature_type == "floor" then
feature = options.layout_floor_type
elseif feature_type == "wall" then
feature = options.layout_wall_type or "rock_wall"
elseif feature_type == "space" then
feature = "space"
end
local space = (feature == "space")
local solid = not space and not (feat.has_solid_floor(feature) or feat.is_door(feature))
local wall = feat.is_wall(feature)
return feature,space,solid,wall
end
-- Maps draw positions onto a unit square <0,0> to <1,1>
-- TODO: Something is going wrong with this algorithm because a 4x4 circle shouldn't look
-- like a square, but I don't see anything wrong with the math.
function hyper.paint.map_to_unit(x,y,item)
local sx,sy = item.corner2.x - item.corner1.x,item.corner2.y - item.corner1.y
-- Slightly scale down the input grid to add 0.5 squares padding on all sides. This should solve accuracy errors
-- we otherwise get at the edges of shapes from attempting to draw a continuous geometric shape onto a discrete grid.
local rx,ry = (x - item.corner1.x) * (sx-1)/sx + 0.5,(y - item.corner1.y) * (sy-1)/sy + 0.5
-- Finally map onto the unit square
return rx/sx,ry/sy
end
-- Determine if a point is inside an oval
-- TODO: http://en.wikipedia.org/wiki/Midpoint_circle_algorithm
-- http://en.wikipedia.org/wiki/Midpoint_circle_algorithm
-- Using a variant of that algorithm and adapt to ovals might give more reliable results and be quicker
function hyper.paint.inside_oval(x,y,item)
-- Circle test
local ax,ay = hyper.paint.map_to_unit(x,y,item)
return (math.pow(ax * 2 - 1,2) + math.pow(ay * 2 - 1,2)) <= 1
end
-- Determine if a point is inside a simple trapeze
-- TODO: Handle rotation and allow offsetting the top and bottom on the x axis for more complex shapes
function hyper.paint.inside_trapese(x,y,item)
local width1 = item.width1
local width2 = item.width2
if width1 == nil then width1 = 0 end
if width2 == nil then width2 = 1 end
local ax,ay = hyper.paint.map_to_unit(x,y,item)
local expected_width = ay * width2 + (1-ay) * width1
return math.abs(ax-0.5) < (expected_width / 2)
end
function hyper.paint.inside_custom(x,y,item)
local ax,ay = hyper.paint.map_to_unit(x,y,item)
return item.shape_type(x,y,ax,ay,item)
end
function hyper.paint.determine_usage_from_layout(layout_grid,options)
usage_restricted_count = 0
usage_open_count = 0
usage_eligible_count = 0
usage_none_count = 0
local gxm, gym = layout_grid.width,layout_grid.height
local usage_grid = hyper.usage.new_usage(gxm,gym)
for x = 0, gxm-1, 1 do
for y = 0, gym-1, 1 do
-- We need to know the local layout grid around this square
local local_grid = { }
-- This flag will track if there is only floor in the area
local only_floor = true
for yl = -options.min_distance_from_wall, options.min_distance_from_wall, 1 do
local_grid[yl] = { }
for xl = -options.min_distance_from_wall, options.min_distance_from_wall, 1 do
local cell = get_layout(layout_grid,x + xl,y + yl)
local_grid[yl][xl] = cell
if cell.solid then only_floor = false end
end
end
-- Completely open floor so we could place a room here
if only_floor == true then
set_usage(usage_grid,x,y, { usage = "open" })
else
-- Are we dealing with floor or wall?
if local_grid[0][0].solid then -- Wall
local function gridsum(grid,list)
local sum = 0
for i,pos in ipairs(list) do
if grid[pos.y][pos.x].solid then sum = sum + 1 end
end
return sum
end
-- A wall can either be usage "none" meaning parts of a room could later be built over it;
-- or it can be "eligible" meaning it can be used as a connecting wall/door to a room;
-- or it can be "restricted" meaning its geometry is not suited for a connecting wall or it has already been used
-- We don't need to cover all cases of complex geometry since for now the layouts will be
-- making simple large blocks. If complex geometry ever gets used it doesn't matter if we flag some squares
-- as eligible when they don't really work because room placement will still vetoe if it can't find a clear area of
-- open or none. It's more important to find all squares that *could* be eligible.
-- Sum the adjacent squares
local adjacent_sum = gridsum(local_grid, { {x=-1,y=0},{x=1,y=0},{x=0,y=-1},{x=0,y=1} })
-- Eligible squares have floor on only one side
-- This ignores diagonals (which is where complex geometry will produce some eligible squares that aren't
-- really eligible). But it's complicated because we're after diagonals only on the side where the floor is.
-- What we need to know in the usage grid is the normal, i.e. the direction in which the user will be entering
-- the room.
if adjacent_sum == 3 then
-- Floor to the north
if not local_grid[-1][0].solid then
set_usage(usage_grid,x,y, { usage = "eligible", normal = { x = 0, y = 1 }, depth = 1})
end
-- Floor to the south
if not local_grid[1][0].solid then
set_usage(usage_grid,x,y, { usage = "eligible", normal = { x = 0, y = -1 }, depth = 1})
end
-- Floor to the west
if not local_grid[0][-1].solid then
set_usage(usage_grid,x,y, { usage = "eligible", normal = { x = 1, y = 0 }, depth = 1})
end
-- Floor to the east
if not local_grid[0][1].solid then
set_usage(usage_grid,x,y, { usage = "eligible", normal = { x = -1, y = 0 }, depth = 1})
end
else
-- Wall all around?
if adjacent_sum == 4 then
local diagonal_sum = gridsum(local_grid, { {x=-1,y=-1},{x=1,y=-1},{x=-1,y=1},{x=1,y=1} })
-- Wall mostly all around? (We allow one missing corner otherwise rooms can't overlap corners
-- and logically it's fine for any wall to be placed there, other missing holes will fail the placement
-- anyway)
if diagonal_sum >= 3 then
-- Should have been set this way at initialization but let's check anyway
set_usage(usage_grid,x,y, { usage = "none" })
else
-- There are some diagonal holes so we can't use this square
set_usage(usage_grid,x,y, { usage = "restricted" })
end
end
end
else -- Floor
-- We already know there is a wall nearby, so this square is restricted
set_usage(usage_grid,x,y, { usage = "restricted", reason = "border" })
end
end
end
end
return usage_grid
end
function hyper.paint.paint_vaults_layout(paint, options, layout_grid)
-- Default options
if options == nil then options = vaults_default_options() end
-- Pick wall type from spread in config
local wall_type = "stone_wall"
if options.wall_type ~= nil then wall_type = options.wall_type end
if options.layout_wall_weights ~= nil then
local chosen = util.random_weighted_from("weight", options.layout_wall_weights)
wall_type = chosen.feature
end
-- Store it in options so it can be used for room surrounds also
options.layout_wall_type = wall_type
local gxm, gym = dgn.max_bounds()
layout_grid = new_layout(gxm,gym) -- Will contain data about how each square is used and therefore how rooms can be applied
paint_grid(paint,options,layout_grid) -- Paint fills onto the layout grid
local usage_grid = hyper.paint.determine_usage_from_layout(layout_grid,options) -- Analyse the layout to determine usage
-- Apply to the actual dungeon grid
for x = 0, gxm-1, 1 do
for y = 0, gym-1, 1 do
local cell = get_layout(layout_grid,x,y)
if cell.feature ~= nil and cell.feature ~= "space" then
dgn.grid(x,y,cell.feature)
elseif cell.feature == nil then
-- Make sure we set the right type of wall in unpainted grids
dgn.grid(x,y,wall_type)
end
end
end
return usage_grid
end
|