feat: expand builder
This commit is contained in:
1 parent
ef58d96008
commit
6d0666042c
1 file changed
+173
+173
@@ -509,6 +509,179 @@ function Builder.build(depth, rng, options)
|
|||||||
local vaultId = "vault" .. depth
|
local vaultId = "vault" .. depth
|
||||||
local openSide = reachSet(map, upX, upY, false, false)
|
local openSide = reachSet(map, upX, upY, false, false)
|
||||||
local leverCount = rng:range(2, 3)
|
local leverCount = rng:range(2, 3)
|
||||||
|
for _ = 1, leverCount do
|
||||||
|
local x, y = pickFree(reachable, rng, function(cx, cy)
|
||||||
|
return freeFloor(cx, cy) and openSide[map:index(cx, cy)] == true
|
||||||
|
end)
|
||||||
|
if x then
|
||||||
|
map:set(x, y, Tiles.LEVER)
|
||||||
|
vaultLevers[#vaultLevers + 1] = { x = x, y = y, id = vaultId, pulled = false }
|
||||||
|
mark(x, y)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local farSide = reachSet(map, upX, upY, false, true)
|
||||||
|
local tx, ty
|
||||||
|
if #vaultLevers > 0 then
|
||||||
|
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
|
||||||
|
vaultDoors[#vaultDoors + 1] = { x = cell.x, y = cell.y, id = vaultId, opened = false }
|
||||||
|
floorItems[#floorItems + 1] = { x = tx, y = ty, item = Vaults.treasure(rng, depth) }
|
||||||
|
mark(tx, ty)
|
||||||
|
else
|
||||||
|
-- No lever site or no cell the door actually gates: back out to a
|
||||||
|
-- plain closed door so the puzzle is never unsolvable or hollow, and
|
||||||
|
-- free the lever tiles this attempt already claimed.
|
||||||
|
map:set(cell.x, cell.y, Tiles.DOOR_CLOSED)
|
||||||
|
for _, lever in ipairs(vaultLevers) do
|
||||||
|
map:set(lever.x, lever.y, Tiles.FLOOR)
|
||||||
|
end
|
||||||
|
vaultLevers = {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Rune stones dot the dungeon from depth three on, rarer than a herb but
|
||||||
|
-- commoner than a trap kit: raw components for runewords.lua's
|
||||||
|
-- socket-and-match combo system, found rather than crafted so a build is
|
||||||
|
-- bounded by what the floor actually offers. Drawn dead last of all,
|
||||||
|
-- after the vault above, so it can never perturb any earlier system's
|
||||||
|
-- rng sequence.
|
||||||
|
if depth >= 3 and rng:chance(1, 2) then
|
||||||
|
local x, y = pickFree(reachable, rng, freeFloor)
|
||||||
|
if x then
|
||||||
|
local key = Runewords.RUNES[rng:range(1, #Runewords.RUNES)]
|
||||||
|
floorItems[#floorItems + 1] = { x = x, y = y, item = Runewords.rune(key) }
|
||||||
|
mark(x, y)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A floor-wide weather condition, drawn dead last of all so it can never
|
||||||
|
-- perturb any earlier system's rng sequence: an infested reading retroactively
|
||||||
|
-- bumps every monster already placed on this floor (exactly like Elites.
|
||||||
|
-- maybePromote's own retroactive stat edit above), since only a stat and
|
||||||
|
-- never a position is at stake.
|
||||||
|
local weather
|
||||||
|
if depth >= 2 and rng:chance(1, 3) then
|
||||||
|
weather = Atmosphere.order[rng:range(1, #Atmosphere.order)]
|
||||||
|
local attackBonus = Atmosphere.monsterAttackBonus(weather)
|
||||||
|
if attackBonus > 0 then
|
||||||
|
for _, entity in ipairs(entities) do
|
||||||
|
entity.attack = entity.attack + attackBonus
|
||||||
|
entity.baseAttack = entity.baseAttack + attackBonus
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ore veins and gem seams dot the dungeon from the second floor on, dead
|
||||||
|
-- last of all so they can never perturb any earlier system's rng sequence:
|
||||||
|
-- unlike the herbs above, a single visit does not spend a node outright -
|
||||||
|
-- it holds a few charges and slowly reopens (see nodes.lua) - so a floor
|
||||||
|
-- already cleared of monsters still rewards a return trip.
|
||||||
|
local nodes = {}
|
||||||
|
if depth >= 2 then
|
||||||
|
local veinCount = rng:range(1, 2)
|
||||||
|
for _ = 1, veinCount do
|
||||||
|
local x, y = pickFree(reachable, rng, freeFloor)
|
||||||
|
if x then
|
||||||
|
local kind = rng:chance(1, 2) and Nodes.ORE or Nodes.GEM
|
||||||
|
local element = kind == Nodes.GEM and Crafting.elements[rng:range(1, #Crafting.elements)] or nil
|
||||||
|
local node = Nodes.new(kind, element)
|
||||||
|
node.x, node.y = x, y
|
||||||
|
nodes[#nodes + 1] = node
|
||||||
|
mark(x, y)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A single item-set piece dots the dungeon from depth three on, rarer
|
||||||
|
-- than a rune but commoner than an artifact: unlike an artifact's fixed
|
||||||
|
-- always-on mechanic, a set piece is inert alone and only pays off once
|
||||||
|
-- its match turns up too (see sets.lua). Drawn dead last of all, after
|
||||||
|
-- the nodes above, so it can never perturb any earlier system's rng
|
||||||
|
-- sequence.
|
||||||
|
if depth >= 3 and rng:chance(1, 5) then
|
||||||
|
local x, y = pickFree(reachable, rng, freeFloor)
|
||||||
|
if x then
|
||||||
|
local entry = Sets.order[rng:range(1, #Sets.order)]
|
||||||
|
floorItems[#floorItems + 1] = { x = x, y = y, item = Sets.create(entry.set, entry.slot) }
|
||||||
|
mark(x, y)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A single mimic disguises itself as loot from depth two on, rarer than
|
||||||
|
-- a trap kit: unlike every item placed above, it is inert to look at and
|
||||||
|
-- only bites once the player actually reaches for it (see mimics.lua).
|
||||||
|
-- Drawn dead last of all, after the set piece above, so it can never
|
||||||
|
-- perturb any earlier system's rng sequence.
|
||||||
|
if depth >= 2 and rng:chance(1, 6) then
|
||||||
|
local x, y = pickFree(reachable, rng, freeFloor)
|
||||||
|
if x then
|
||||||
|
floorItems[#floorItems + 1] = { x = x, y = y, item = Mimics.floorItem(rng) }
|
||||||
|
mark(x, y)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A scroll of remove curse turns up rarer than a mimic, the one dedicated
|
||||||
|
-- remedy for the curse pass below that a run can actually find rather than
|
||||||
|
-- having to gamble on a shrine of fortune. Drawn dead last of all, after
|
||||||
|
-- the mimic above, so it can never perturb any earlier system's rng
|
||||||
|
-- sequence.
|
||||||
|
if depth >= 2 and rng:chance(1, 8) then
|
||||||
|
local x, y = pickFree(reachable, rng, freeFloor)
|
||||||
|
if x then
|
||||||
|
floorItems[#floorItems + 1] = { x = x, y = y, item = Items.scroll("removecurse") }
|
||||||
|
mark(x, y)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Curses.lua's dark mirror of affixes.lua rolls dead last of all, after
|
||||||
|
-- every item above has already been placed: it only edits the stats of
|
||||||
|
-- weapons, armor, rings and bows already sitting in floorItems, never a
|
||||||
|
-- position, so it cannot desync reachability - but rng-ordering discipline
|
||||||
|
-- still applies, since it draws from the same stream every earlier
|
||||||
|
-- placement in this floor already relied on. Skips anything that already
|
||||||
|
-- landed a beneficial affix or names its own set/artifact, so a single
|
||||||
|
-- drop is never simultaneously a blessing and a curse.
|
||||||
|
for _, ground in ipairs(floorItems) do
|
||||||
|
local slot = slotForCursableItem[ground.item.kind]
|
||||||
|
if slot and not ground.price and not ground.item.affix and not ground.item.set and not ground.item.artifact then
|
||||||
|
local curse = Curses.roll(rng, slot)
|
||||||
|
if curse then Curses.afflict(ground.item, curse) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A miner's pick turns up rarer than the remove-curse scroll above, the
|
||||||
|
-- one way a run can find its own way into a rock burrower's tunnel or dig
|
||||||
|
-- a shortcut through a dead-end wall (see digging.lua). Drawn dead last
|
||||||
|
-- of all, after the curse pass above, so it can never perturb any
|
||||||
|
-- earlier system's rng sequence.
|
||||||
|
if depth >= 2 and rng:chance(1, 10) then
|
||||||
|
local x, y = pickFree(reachable, rng, freeFloor)
|
||||||
|
if x then
|
||||||
|
floorItems[#floorItems + 1] = { x = x, y = y, item = Items.pick() }
|
||||||
|
mark(x, y)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A lockpick turns up commoner than the miner's pick above, drawn dead
|
||||||
|
-- last of all so it can never perturb any earlier system's rng sequence:
|
||||||
|
-- the one way a run can gamble past a locked chest's lock without ever
|
||||||
|
-- finding its key (see chests.lua's Chests.pick).
|
||||||
|
if rng:chance(1, 4) then
|
||||||
|
local x, y = pickFree(reachable, rng, freeFloor)
|
||||||
|
if x then
|
||||||
|
floorItems[#floorItems + 1] = { x = x, y = y, item = Items.lockpick() }
|
||||||
|
mark(x, y)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 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
|
||||||
return Level.new({
|
return Level.new({
|
||||||
depth = depth,
|
depth = depth,
|
||||||
generator = generator,
|
generator = generator,
|
||||||
|
|||||||
Reference in new issue
Block a user