feat: extend builder with onOpenSide
This commit is contained in:
1 parent
6d0666042c
commit
364ea34db4
1 file changed
+173
+173
@@ -682,6 +682,179 @@ function Builder.build(depth, rng, options)
|
||||
-- A single lootable chest dots the dungeon from the first floor on, rarer
|
||||
-- than a lockpick, drawn dead last of all so it can never perturb any
|
||||
-- earlier system's rng sequence: a locked one may also be trapped (see
|
||||
-- Chests.new), so a run either finds a key, gambles a lockpick, or leaves
|
||||
-- it be.
|
||||
local chests = {}
|
||||
if rng:chance(1, 3) then
|
||||
local x, y = pickFree(reachable, rng, freeFloor)
|
||||
if x then
|
||||
local chest = Chests.new(rng, depth)
|
||||
chest.x, chest.y = x, y
|
||||
chests[#chests + 1] = chest
|
||||
mark(x, y)
|
||||
end
|
||||
end
|
||||
|
||||
-- A secret door hides a single alcove of bonus loot behind an otherwise
|
||||
-- ordinary stretch of wall, found only by lingering beside it long enough
|
||||
-- (see secrets.lua's passive sweep). Drawn dead last of all before the
|
||||
-- grave below - which must stay the true last word, since it is the one
|
||||
-- block whose own rng draw count depends on what the caller passed in
|
||||
-- (see its own comment) - so this can never perturb any earlier system's
|
||||
-- rng sequence either: picks an already-reachable floor cell and a
|
||||
-- direction whose neighbour and whose cell two further along are both
|
||||
-- still untouched solid rock, then carves that far cell into a one-tile
|
||||
-- room holding a single roll of loot. The door itself stays a plain wall
|
||||
-- tile until secrets.lua's sweep discovers it - this list only remembers
|
||||
-- where to look. Skipped outright whenever 40 random reachable cells in a
|
||||
-- row turn up no such spot, so a cramped floor's own geometry never
|
||||
-- forces the roll to place nothing.
|
||||
local secretDoors = {}
|
||||
if depth >= 2 and rng:chance(1, 4) then
|
||||
for _ = 1, 40 do
|
||||
local cell = reachable[rng:range(1, #reachable)]
|
||||
local dir = NEIGHBOURS[rng:range(1, #NEIGHBOURS)]
|
||||
local doorX, doorY = cell.x + dir[1], cell.y + dir[2]
|
||||
local roomX, roomY = cell.x + dir[1] * 2, cell.y + dir[2] * 2
|
||||
if roomX >= 2 and roomY >= 2 and roomX <= map.width - 1 and roomY <= map.height - 1
|
||||
and map:get(doorX, doorY) == Tiles.WALL and map:get(roomX, roomY) == Tiles.WALL then
|
||||
-- The room's only walkable neighbour may be the door itself: if any
|
||||
-- of its other three sides already happens to border open floor
|
||||
-- somewhere else in the layout, that side would let the player
|
||||
-- wander straight into the alcove without ever finding the door,
|
||||
-- so a candidate site with any such back way in is rejected outright.
|
||||
local isolated = true
|
||||
for _, side in ipairs(NEIGHBOURS) do
|
||||
local ax, ay = roomX + side[1], roomY + side[2]
|
||||
if not (ax == doorX and ay == doorY) and map:isWalkable(ax, ay) then
|
||||
isolated = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if isolated then
|
||||
map:set(roomX, roomY, Tiles.FLOOR)
|
||||
secretDoors[#secretDoors + 1] = { x = doorX, y = doorY, discovered = false }
|
||||
floorItems[#floorItems + 1] = { x = roomX, y = roomY, item = rollItem(rng, depth) }
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- A biome reinforcement and hazard bloom, drawn dead last of all before
|
||||
-- the grave below - which must stay the true last word, since it is the
|
||||
-- one block whose own rng draw count depends on what the caller passed in
|
||||
-- (see its own comment) - so this can never perturb any earlier system's
|
||||
-- rng sequence either: see biomes.lua for why a floor's biome is a fixed
|
||||
-- function of depth rather than another dice roll like weather. The
|
||||
-- reinforcement draws only from natives of the eligible roster already
|
||||
-- computed above, so it can never plant a monster too deep for its own
|
||||
-- minDepth, and starts dormant exactly like every other wild monster
|
||||
-- placed above (see Stealth.DORMANT); the bloom seeds level.hazards the
|
||||
-- same way a fire spell or a trap would mid-game, just already
|
||||
-- smouldering the moment the floor is first entered.
|
||||
local biome = Biomes.forDepth(depth)
|
||||
local natives = Biomes.nativesAmong(biome, eligible)
|
||||
if #natives > 0 and rng:chance(1, 3) then
|
||||
local x, y = pickFree(reachable, rng, freeFloor)
|
||||
if x then
|
||||
local spec = natives[rng:range(1, #natives)]
|
||||
local monster = Monsters.create(spec, #entities + 1, x, y, depth)
|
||||
monster.awareness = Stealth.DORMANT
|
||||
entities[#entities + 1] = monster
|
||||
mark(x, y)
|
||||
end
|
||||
end
|
||||
|
||||
local hazards = {}
|
||||
if rng:chance(1, 4) then
|
||||
local x, y = pickFree(reachable, rng, freeFloor)
|
||||
if x then
|
||||
hazards[#hazards + 1] = { x = x, y = y, kind = biome.hazardKind, duration = 4, potency = 2 }
|
||||
end
|
||||
end
|
||||
|
||||
-- A pressure gate seals a second stash behind a shove puzzle instead of a
|
||||
-- lever pull: a gate set on an unused doorway pinch point that only stays
|
||||
-- open while a block's weight rests on its linked plate (see blocks.lua),
|
||||
-- rather than a lever's one-way, permanently-spent pull. The block always
|
||||
-- sits exactly two cells from its plate along a single cardinal direction,
|
||||
-- with the cell behind the block itself also free, so a single shove from
|
||||
-- that approach cell always lands it on the plate - the puzzle is never a
|
||||
-- multi-step arrangement a floor's own cramped geometry might make
|
||||
-- unsolvable. Also verifies, after sealing the gate shut, that the stairs
|
||||
-- down are still reachable without ever crossing it - exactly the same
|
||||
-- guarantee the locked-door pass above gives a key - so a gate can only
|
||||
-- ever seal optional treasure, never the one path through the floor.
|
||||
-- Falls back to an ordinary closed door whenever the floor's geometry
|
||||
-- cannot host a genuine puzzle (no free doorway, no straight three-cell
|
||||
-- run for the plate/block/approach, or the gate would seal the only route
|
||||
-- to the stairs), so a gate never seals content behind an unsolvable or
|
||||
-- pointless door. Drawn dead last of all before the grave below - which
|
||||
-- must stay the true last word (see its own comment) - so this can never
|
||||
-- perturb any earlier system's rng sequence either.
|
||||
local blocks, plates, gates = {}, {}, {}
|
||||
if depth >= 3 and rng:chance(1, 5) then
|
||||
local doorCandidates = doorwayCandidates(map)
|
||||
for i = #doorCandidates, 1, -1 do
|
||||
if taken(doorCandidates[i].x, doorCandidates[i].y) then
|
||||
table.remove(doorCandidates, i)
|
||||
end
|
||||
end
|
||||
if #doorCandidates > 0 then
|
||||
local cell = doorCandidates[rng:range(1, #doorCandidates)]
|
||||
map:set(cell.x, cell.y, Tiles.GATE_CLOSED)
|
||||
mark(cell.x, cell.y)
|
||||
local sealsTheOnlyRoute = not reachSet(map, upX, upY, false)[map:index(downX, downY)]
|
||||
local openSide = reachSet(map, upX, upY, false, false)
|
||||
local function onOpenSide(cx, cy)
|
||||
return freeFloor(cx, cy) and openSide[map:index(cx, cy)] == true
|
||||
end
|
||||
local px, py = pickFree(reachable, rng, onOpenSide)
|
||||
local siteDirs = {}
|
||||
if px then
|
||||
for _, dir in ipairs(NEIGHBOURS) do
|
||||
local bx, by = px - dir[1], py - dir[2]
|
||||
local ax, ay = bx - dir[1], by - dir[2]
|
||||
if onOpenSide(bx, by) and onOpenSide(ax, ay) then
|
||||
siteDirs[#siteDirs + 1] = { bx = bx, by = by }
|
||||
end
|
||||
end
|
||||
end
|
||||
local tx, ty
|
||||
if not sealsTheOnlyRoute and #siteDirs > 0 then
|
||||
local farSide = reachSet(map, upX, upY, false, true)
|
||||
tx, ty = pickFree(reachable, rng, function(cx, cy)
|
||||
return freeFloor(cx, cy) and farSide[map:index(cx, cy)] == true
|
||||
and openSide[map:index(cx, cy)] ~= true
|
||||
end)
|
||||
end
|
||||
if tx then
|
||||
local site = siteDirs[rng:range(1, #siteDirs)]
|
||||
plates[#plates + 1] = { x = px, y = py, gateX = cell.x, gateY = cell.y }
|
||||
blocks[#blocks + 1] = { x = site.bx, y = site.by }
|
||||
gates[#gates + 1] = { x = cell.x, y = cell.y, open = false }
|
||||
mark(px, py)
|
||||
mark(site.bx, site.by)
|
||||
floorItems[#floorItems + 1] = { x = tx, y = ty, item = Blocks.treasure(rng, depth) }
|
||||
mark(tx, ty)
|
||||
else
|
||||
map:set(cell.x, cell.y, Tiles.DOOR_CLOSED)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- A hand-authored prefab vault dots the deep dungeon from depth three on,
|
||||
-- sealed behind solid rock rather than gated by a key or a lever: the one
|
||||
-- way in is digging.lua's pick (or a burrower's own claws), swung from
|
||||
-- whichever reachable cell happens to border its outer wall. Anchored only
|
||||
-- over untouched rock - every cell of its footprint still the generator's
|
||||
-- own default fill - so it can never overwrite a corridor or room this
|
||||
-- floor's own generator already carved, and only accepted once at least
|
||||
-- one of its wall cells actually borders the reachable area, so a vault
|
||||
-- can never end up sealed unreachably deep inside solid rock no digger
|
||||
-- could ever find. Its monster and loot markers (see gen/prefab.lua) draw
|
||||
-- from the same eligible roster and item tables every earlier pass in this
|
||||
return Level.new({
|
||||
depth = depth,
|
||||
generator = generator,
|
||||
|
||||
Reference in new issue
Block a user