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 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431
|
------------------------------------------------------------------------------
-- v_place.lua:
--
-- Functions for selecting vaults, generating rooms from code, analysing for
-- geometry and connectability, and finding a placement position on the
-- usage grid.
--
------------------------------------------------------------------------------
-- GENERAL TODO:
-- 2. CAN FIX WITH STRATEGIES: Open room placement is slow. When we place an open room we should set ineligible squares all around it (at e.g. 2 tiles distance). This will reduce
-- attempts to place overlapping rooms and reduce the overall number of 'open' spots, meaning we get increasingly more likely to hit an 'eligible_open' instead.
-- These new restricted zones still need to reference the room so they don't stop proper attachment of new rooms.
-- 3. CAN FIX WITH _process_disconnected_zones: Funny-shaped rooms can still produce VETO-causing bubbles even with all this checking, this can typically happen when two funny-shaped rooms connect
-- via two separate walls, creating an enclosed spot in between. Thankfully this doesn't happen very often (yet). There are two ways to avoid this: a) Relax
-- the VETO restriction, this shouldn't actually be a problem because Crawl automatically generates trapdoors anyway! Crawl could instead / additionally fill the area with
-- no_tele_into (but on its own this doesn't solve the problem that an off-level hatch/shaft could land the player there, and possibly end them up in an a multi-level
-- hatch loop!) b) Actually detect and handle such enclosed areas during layout generation. This sounds hard to do in a performance-friendly way but it only needs
-- a check when we're attaching to an open_eligible, need to check each tile that's still open for connectivity to the room we just placed (or some sort of flood fill).
-- 4. General performance - it's definitely ok in V but some of the experimental layouts I've tried have more problems. A big factor is needing to eliminate more eligible
-- spots when we place rooms because once a level is really busy most placement attempts are doomed. When we place a room we should send out rays in the directions
-- of normals and if we hit something close (or map edge) then that wall is no longer eligible. The trigger proximity needs to be 1 less than the smallest room that can fit.
-- Can be improved with custom placement strategies and usage filters. Also it's a good idea to pick a room first rather than picking walls, we can weight more towards
-- rooms with less connections (room_sort/room_pick callbacks)
-- 5. Additional corridor / door carving. Send rays out from walls (in combination with the above) and if a room is within a suitable proximity (e.g. < 5) then we can
-- carve out a corridor and doors. Another strategy is to place all rooms first and then draw paths to other rooms (like roguey).
-- 6. Room flipping. Should be fairly straightforward now; we need to a) invert the x or y normal dependending on which way we're flipping, b) appropriately adjust the logic
-- of vault placement and c) similarly flip the final usage normals. I'm just hesitant to throw this spanner in the works before everything else is 100% working.
hyper.place = {}
------------------------------------------------------------------------------
-- Main public functions.
-- These functions are the main build methods we will use, that get called by
-- items in the build table.
-- Build a series of rooms given a build table
function hyper.place.build_rooms(build,usage_grid,options)
-- Init local variables
local results, rooms_placed, times_failed, total_failed, room_count, total_rooms_placed = { }, 0, 0, 0, build.max_rooms or options.max_rooms or 10, options.rooms_placed or 0
if hyper.profile then
profiler.push("PlaceRoom", { failed = times_failed })
end
local post_place = (build.post_placement_callback or options.post_placement_callback)
-- Keep trying to place rooms until we've had max_room_tries fail in a row or we reach the max
while rooms_placed < room_count and times_failed < options.max_room_tries do
local placed = false
-- Pick and construct a room
local room = hyper.rooms.pick_room(build,options)
-- Attempt to place it. The placement function will try several times to find somewhere to fit it. If it fails
-- then we generate another room and try again until placement has failed max_room_tries in a row.
if room ~= nil then
if room.grid == nil then
print ("Room grid empty! Pass: "..build.pass..", Generator: " .. room.chosen_generator.generator .. ", " .. (room.chosen_generator.name or room.chosen_generator.tag))
return results
end
local result = hyper.place.place_room(room,build,usage_grid,options)
if result ~= nil and result.placed then
placed = true
-- Increment rooms placed in this build, and total rooms placed by all builds
-- TODO: Could increment this automatically when applying instead...
rooms_placed = rooms_placed + 1
total_rooms_placed = total_rooms_placed + 1
options.rooms_placed = total_rooms_placed
-- Perform analysis for stairs (and perform inner wall substitution if applicable)
if post_place ~= nil then post_place(usage_grid,room,result,options) end
table.insert(results,result)
-- Increment the count of rooms of this type
if room.generator_used ~= nil then
if room.generator_used.placed_count == nil then room.generator_used.placed_count = 0 end
room.generator_used.placed_count = room.generator_used.placed_count + 1
end
if hyper.profile then
profiler.pop()
profiler.push("PlaceRoom", { failed = total_failed, failed_in_a_row = times_failed })
end
times_failed = 0 -- Reset fail count
end
end
if not placed then
times_failed = times_failed + 1 -- Increment failure count
total_failed = total_failed + 1
end
-- dump_usage_grid_v3(usage_grid)
end
if hyper.profile then
profiler.pop()
profiler.push("PlacedRooms", { failed = total_failed, placed = rooms_placed })
profiler.pop()
end
-- TODO: Return some useful stats in results (total placements, total failures, so forth)
return results
end
-- Places a room onto the grid
function hyper.place.place_room(room,build,usage_grid,options)
local tries = 0
local done = false
local state
if hyper.profile then
profiler.push("PlaceRoom")
end
-- TODO: Can optimise this (and a few other similarly cascading selectors) by copying such options down onto the generator.
-- TODO: Further to this, could be handy to override individual callbacks in the strategy rather than just the whole thing
local strategy = room.generator_used.strategy or build.strategy or options.stategy or hyper.place.strategy_default
if strategy == nil then
print("No strategy: " .. (build.pass or "[unknown]"))
return { placed = false }
end
-- Have a few goes at placing; we'll select a random eligible spot each time and try to place the vault there
while tries < options.max_place_tries and not done do
tries = tries + 1
-- Let the place picker find us a spot
local place = strategy.pick_place(room,build,usage_grid,options)
if not place or place == nil then return { placed = false } end
-- Now, pick an anchor on the given room
local anchor = strategy.pick_anchor(place,room,build,usage_grid,options)
if not anchor or anchor == nil then return { placed = false } end
-- Attempt to place the room on the wall at a random position
state = hyper.place.process_room_place(anchor, place, room, strategy, build, usage_grid, options)
if state and hyper.place.apply_room(state,room,build,usage_grid,options) then
done = true
end
end
if not done then return { placed = false } end
if hyper.profile then
profiler.pop()
end
return { placed = done, coords_list = state.coords, state = state }
end
-- For a given position and room orientation determines what grid features this room is overlapping
-- and therefore whether we're allowed to place it (depending on how vetoes etc. are set up)
function hyper.place.process_room_place(anchor, place, room, strategy, build, usage_grid, options)
local pos,usage,dir,origin = place.pos,place.usage,anchor.dir,anchor.origin
-- Figure out the mapped x and y vectors of the room relative to its orient
local room_final_x_dir = (dir - 1) % 4
local room_final_y_dir = (dir - 2) % 4
local room_final_x_normal = vector.normals[room_final_x_dir + 1]
local room_final_y_normal = vector.normals[room_final_y_dir + 1]
-- Now we can use those vectors along with the position of the connecting wall within the room, to find out where the (0,0) corner
-- of the room lies on the map (and use that coord for subsequent calculations within the room grid)
local room_base = vector.add_mapped(pos, { x = -(origin.x), y = -(origin.y) }, room_final_x_normal, room_final_y_normal)
-- State object to send to callbacks
local state = {
anchor = anchor,
room = room,
usage = usage,
pos = pos,
wall = chosen_wall,
base = room_base,
dir = dir,
build = build,
options = options,
normals = {
x = room_final_x_normal,
y = room_final_y_normal
}
}
-- Layout configuration can now veto this room placement with a callback
local veto_place = options.veto_place_callback or strategy.veto_place
if veto_place ~= nil then
local result = veto_place(state,usage_grid)
if result == true then return false end
end
-- Loop through all coords of the room and its walls. Map to grid coords and construct a list of relevant coords from this,
-- to make it really fast to loop through on subsequent occasions.
local coords_list = {}
state.coords = coords_list
local is_clear = true
local place_check = room.generator_used.veto_cell or build.veto_cell or build.strategy.veto_cell or options.veto_cell or hyper.place.cell_veto_normal
for m = 0, room.size.y-1, 1 do
for n = 0, room.size.x-1, 1 do
local coord = { room_pos = { x = n, y = m } }
coord.grid_pos = vector.add_mapped(room_base, coord.room_pos, room_final_x_normal, room_final_y_normal)
coord.grid_usage = hyper.usage.get_usage(usage_grid, coord.grid_pos.x, coord.grid_pos.y)
coord.room_cell = hyper.usage.get_usage(room.grid,coord.room_pos.x,coord.room_pos.y)
-- print ("Checking: " .. n .. ", " .. m .. " @ " .. coord.grid_pos.x .. ", " .. coord.grid_pos.y )
-- Out of bounds is instant fail, although even this we *could* override sometimes if we're careful...
-- TODO: This is actually checked in cell_veto_standard, we don't need this now right?
if (not coord.room_cell.space or coord.room_cell.buffer) and coord.grid_usage == nil then
is_clear = false
break
end
if place_check ~= nil and place_check(coord, state) then
is_clear = false
break
end
-- Remember non-space and non-vault coords for later. Our veto function might have allowed this room to be placed
-- despite a vault already existing but we definitely don't want to consider it valid for overwriting later.
-- TODO: Actually we could use space cells to update usage later to implement buffers
if not coord.room_cell.space and not coord.grid_usage.vault then
table.insert(coords_list, coord)
end
end
if not is_clear then break end
end
-- Something vetoed this position; the function fails and we'll look for a new spot
if not is_clear then
return false
end
return state
end
-- Actually apply the room to the dungeon and update the usage grid
function hyper.place.apply_room(state,room,build,usage_grid,options)
-- Unpack variables
local usage,coords_list,room_base,final_orient = state.usage,state.coords,state.base,state.dir
-- Wall type. Originally this was randomly varied but it didn't work well when rooms bordered.
-- TODO: Wall type should be handled in transform / decorator / post-process ...
room.wall_type = options.layout_wall_type or "rock_wall"
-- Now ... loop through every square of the room; update the dungeon grid and wall usage table, and make a list
-- of which connectable walls overlap with another vault (Note: we should have already made that list during
-- placement processing because it might be relevant for vetos.)
-- These walls can be the original orientation wall, 'incidental' walls where the room just
-- happened to overlap another room's eligible walls, or for open rooms this will be all sides. Then we
-- pass the lists to decorator functions for painting doors and so forth.
-- We don't have to end up with a door on the original orientation spot, although we will force that at least
-- one cell from that wall gets carved, to ensure connectivity.
-- Room depth
local new_depth = 2
if usage.depth ~= nil then new_depth = usage.depth + 1 end
-- Setup door tables
local incidental_connections = { }
for n = 0, 3, 1 do
incidental_connections[n] = {}
end
local door_connections = { }
for i, coord in ipairs(coords_list) do
-- A vault will be placed here then fill the area with rock. This is so that inner empty space will be rock-filled
-- even in open areas.
-- TODO: For this to work we'd have to have already figured out which empty areas were enclosed.
-- if room.type == "vault" or (room.type == "transform" and coord.room_cell.vault) then
-- dgn.grid(coord.grid_pos.x, coord.grid_pos.y, room.wall_type)
-- end
-- Apply the specific feature onto the dungeon grid.
-- TODO: We shouldn't even perform this or apply the usage yet. Let wall decorators operate first (which themselves
-- will also update usage) and only finally apply the entire grid once we're certain what goes there.
if (room.type == "grid" or room.type == "transform") and not coord.room_cell.space and not coord.room_cell.vault and not options.test then
dgn.grid(coord.grid_pos.x, coord.grid_pos.y, coord.room_cell.feature)
end
local room_cell,grid_cell,grid_coord = coord.room_cell,coord.grid_usage,coord.grid_pos
-- Update directions
-- TODO: Should ensure cells always have anchors then following line isn't needed
if room_cell.anchors == nil then room_cell.anchors = {} end
for a,anchor in ipairs(room_cell.anchors) do
local u_wall_dir = (anchor.normal.dir + final_orient) % 4
anchor.normal = vector.normals[u_wall_dir+1]
anchor.grid_pos = vector.add_mapped(state.base, anchor.pos, state.normals.x, state.normals.y)
end
-- Manage overlays
-- TODO: There is a bunch of stuff we've lost here and need to reinstate....
if room_cell.carvable then
if usage.open_area or not usage.solid then room_cell.open_area = true end
local wall_info = { room_coord = coord.room_pos, grid_pos = grid_coord, usage = grid_usage, cell = room_cell }
if grid_cell.carvable then
-- Once two carvables have overlayed, we should not carve any more
room_cell.carvable = false
room_cell.restricted = true
if grid_cell.room == usage.room then
-- Original wall
table.insert(door_connections, wall_info)
elseif room_cell.anchors ~= nil and room_cell.anchors[1] ~= nil then
-- Incidental overlay
table.insert(incidental_connections[room_cell.anchors[1].normal.dir], wall_info)
end
else
-- Count all open sides as door connections; potentially we'll get doors on multiple side
if not grid_cell.solid then
table.insert(door_connections, wall_info)
end
end
end
-- Update usage
room_cell.depth = new_depth
room_cell.room = room
room_cell.room_dir = final_orient
hyper.usage.set_usage(usage_grid,coord.grid_pos.x,coord.grid_pos.y,room_cell)
end
-- Place a vault map if we have one
local vault_room, vault_bounds
if room.type == "vault" then
vault_room = room
vault_pos = { x = 0, y = 0 }
elseif room.type == "transform" and room.inner_room.type == "vault" then
vault_room = room.inner_room
vault_pos = room.inner_pos
end
if options.test then return coords_list end
if vault_room ~= nil then
-- Lookup the two corners of the room and map to oriented coords to find the top-leftmost and bottom-rightmost coords relative to dungeon orientation.
local v1,v2 = vault_pos,{ x = vault_pos.x + vault_room.size.x - 1, y = vault_pos.y + vault_room.size.y - 1 }
local c1 = vector.add_mapped(room_base,v1,state.normals.x, state.normals.y)
local c2 = vector.add_mapped(room_base,v2,state.normals.x, state.normals.y)
-- Vault origin found by min coords
local origin = { x = math.min(c1.x,c2.x), y = math.min(c1.y,c2.y) }
-- We can only rotate a map clockwise or anticlockwise. To rotate 180 we just flip on both axes.
if final_orient == 0 then dgn.reuse_map(room.vplace,origin.x,origin.y,false,false,0,true,true)
elseif final_orient == 1 then dgn.reuse_map(room.vplace,origin.x,origin.y,false,false,-1,true,true)
elseif final_orient == 2 then dgn.reuse_map(room.vplace,origin.x,origin.y,true,true,0,true,true)
elseif final_orient == 3 then dgn.reuse_map(room.vplace,origin.x,origin.y,false,false,1,true,true) end
end
-- Use decorator callbacks to decorate the connector cells; e.g. doors and windows, solid wall, open wall ...
local decorate_callback = build.wall_decorator or options.decorate_walls_callback or hyper.rooms.decorate_walls
decorate_callback(state,door_connections,true)
-- Have a chance to add doors / windows to each other side of the room
for n = 0, 3, 1 do
if incidental_connections[n] ~= nil and #(incidental_connections[n]) > 0 then
decorate_callback(state,incidental_connections[n],false)
end
end
return coords_list
end
-- TODO: This callback only actually applies to V, right now at least. It's reference as a callback in vaults_default_options and should
-- move to hyper_vaults along with anything else V-specific prior to merge ...?
function hyper.place.analyse_vault_post_placement(usage_grid,room,result,options)
local perform_subst = true
if room.preserve_wall or room.wall_type == nil then perform_subst = false end
result.stairs = { }
for i,coord in ipairs(result.coords_list) do
p = coord.grid_pos
if dgn.in_bounds(p.x,p.y) and
(feat.is_stone_stair(p.x,p.y)) or
-- On V:1 the branch entrant stairs don't count as stone_stair; we need to check specifically for the V exit stairs
-- to avoid overwriting e.g. Crypt or Blade entrance stairs!
dgn.feature_name(dgn.grid(p.x,p.y)) == "exit_vaults" then
-- Remove the stair and remember it
dgn.grid(p.x,p.y,"floor") --TODO: Be more intelligent about how to replace it
table.insert(result.stairs, { pos = { x = p.x, y = p.y } })
end
-- Substitute rock walls for surrounding wall type
if dgn.feature_name(dgn.grid(p.x,p.y)) == "rock_wall" then
dgn.grid(p.x,p.y,room.wall_type)
end
end
end
-- TODO: This is the post-everything function for V so we can be very
-- specific about how stairs are handled. It probably shouldn't be in
-- this include since it relies on the post-placement-analysis from V
-- and typically we want to let stairs place randomly anyway.
function hyper.place.restore_stairs(results, usage_grid, options)
-- Now we need some stairs
local stairs = { }
for i, r in ipairs(results) do
for j, s in ipairs(r.stairs) do
if s ~= nil then table.insert(stairs,s) end
end
end
-- Place three up, three down, plus two of each hatch
local stair_types = {
"stone_stairs_down_i", "stone_stairs_down_ii",
"stone_stairs_down_iii", "stone_stairs_up_i",
"stone_stairs_up_ii", "stone_stairs_up_iii" }
-- We could place some hatches in rooms but from discussion on IRC just let them place naturally -
-- they inherently carry some risk and can't be used for pulling tactics so it's fine to have them in corridors.
-- "escape_hatch_up", "escape_hatch_down",
-- "escape_hatch_up", "escape_hatch_down" }
for n,stair_type in ipairs(stair_types) do
-- Do we have any left?
if #stairs == 0 then break end
-- Any random stair
local i = crawl.random_range(1, #stairs)
local stair = stairs[i]
-- Place it
dgn.grid(stair.pos.x, stair.pos.y, stair_type)
-- Remove from list
table.remove(stairs,i)
end
return true
end
|