From 72e296914618686df5c0425e63f75541f0776d04 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 2 Sep 2026 15:53:52 +0200 Subject: [PATCH] feat: build out builder --- src/builder.lua | 131 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/src/builder.lua b/src/builder.lua index 9618784..5d9cd32 100644 --- a/src/builder.lua +++ b/src/builder.lua @@ -855,14 +855,145 @@ function Builder.build(depth, rng, options) -- 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 + -- floor already used. 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 prefabVaults = {} + if depth >= 3 and rng:chance(1, 5) then + local vault = Prefab.vaults[rng:range(1, #Prefab.vaults)] + local w, h = Prefab.size(vault) + local vaultReach = reachSet(map, upX, upY, false) + for _ = 1, 40 do + local ox = rng:range(2, map.width - w) + local oy = rng:range(2, map.height - h) + if Prefab.fits(map, vault, ox, oy) then + local untouched = true + for y = oy, oy + h - 1 do + for x = ox, ox + w - 1 do + if map:get(x, y) ~= Tiles.WALL then untouched = false end + end + end + local bordersReachable = false + for y = oy - 1, oy + h do + for x = ox - 1, ox + w do + local onPerimeter = x < ox or x > ox + w - 1 or y < oy or y > oy + h - 1 + if onPerimeter and map:inBounds(x, y) and vaultReach[map:index(x, y)] then + bordersReachable = true + end + end + end + if untouched and bordersReachable then + local stamp = Prefab.stamp(map, vault, ox, oy) + for _, spot in ipairs(stamp.treasure) do + floorItems[#floorItems + 1] = { x = spot.x, y = spot.y, item = Vaults.treasure(rng, depth) } + mark(spot.x, spot.y) + end + for _, spot in ipairs(stamp.loot) do + floorItems[#floorItems + 1] = { x = spot.x, y = spot.y, item = rollItem(rng, depth) } + mark(spot.x, spot.y) + end + for _, spot in ipairs(stamp.monsters) do + local spec = eligible[rng:range(1, #eligible)] + local monster = Monsters.create(spec, #entities + 1, spot.x, spot.y, depth) + monster.awareness = Stealth.DORMANT + entities[#entities + 1] = monster + mark(spot.x, spot.y) + end + prefabVaults[#prefabVaults + 1] = { name = stamp.name, x = ox, y = oy, w = w, h = h } + break + end + end + end + end + + -- A net turns up from depth five on, rarer than a miner's pick, drawn dead + -- last of all - after even the prefab vault pass above - so it can never + -- perturb any earlier system's rng sequence: the one way a run carries the + -- means to subdue a battered monster alive instead of finishing it off + -- (see captives.lua), rather than a strictly worse copy of an ordinary + -- weapon. Gated this deep so it can never turn up in the fixed shallow + -- worlds the win-condition tests build with a low --maxDepth. + if depth >= 5 and rng:chance(1, 6) then + local x, y = pickFree(reachable, rng, freeFloor) + if x then + floorItems[#floorItems + 1] = { x = x, y = y, item = Items.net() } + mark(x, y) + end + end + + -- A rift shard turns up from depth four on, rarer than a net, drawn dead + -- last of all - after the net above - so it can never perturb any earlier + -- system's rng sequence: fuel for the one crossing a rift (below) actually + -- costs (see branches.lua), found a couple of floors ahead of where a rift + -- can itself first appear so a run always has a real chance to be carrying + -- one the day it matters. + if depth >= 4 and rng:chance(1, 6) then + local x, y = pickFree(reachable, rng, freeFloor) + if x then + floorItems[#floorItems + 1] = { x = x, y = y, item = Items.riftstone() } + mark(x, y) + end + end + + -- A rift tears open rarer still, only from depth six on, drawn dead last + -- of all - after the shard above - so it can never perturb any earlier + -- system's rng sequence: a shimmering, unstable seam in the floor a + -- carried rift shard alone can stabilize into a crossing onto a short, + -- harsher pocket floor (see branches.lua and Game:enterRift). Plain + -- position-keyed level data exactly like a shrine or a node, never a tile + -- of its own, so it needs no new entry in tiles.lua. + local rifts = {} + if depth >= 6 and rng:chance(1, 8) then + local x, y = pickFree(reachable, rng, freeFloor) + if x then + rifts[#rifts + 1] = { x = x, y = y } + mark(x, y) + end + end + + -- A previous run's grave turns up rarer than a pick, dead last of all - + -- after the lockpick, chest, secret door, biome, gate and net pass above - + -- so it can never perturb any earlier system's rng sequence: cross-run + -- bones (see graves.lua), rolled only when the caller actually supplies + -- any - a run without --meta, or a fresh meta with no recorded deaths + -- yet, makes no extra rng draw here at all, so its floors stay + -- byte-identical to a build with no graves system in play. + if options and options.graves and #options.graves > 0 then + local eligibleGraves = Graves.eligibleFor(options.graves, depth) + if #eligibleGraves > 0 and rng:chance(1, 5) then + local x, y = pickFree(reachable, rng, freeFloor) + if x then + local grave = eligibleGraves[rng:range(1, #eligibleGraves)] + floorItems[#floorItems + 1] = { x = x, y = y, item = Graves.floorItem(grave) } + mark(x, y) + end + end + end + return Level.new({ depth = depth, generator = generator, + weather = weather, + biome = biome.key, map = map, entities = entities, floorItems = floorItems, traps = traps, lights = lights, + hazards = hazards, + shrines = shrines, + questBoards = questBoards, + cauldrons = cauldrons, + nodes = nodes, + chests = chests, + vaultLevers = vaultLevers, + vaultDoors = vaultDoors, + secretDoors = secretDoors, + blocks = blocks, + plates = plates, + gates = gates, + prefabVaults = prefabVaults, + rifts = rifts, dark = dark, explored = {}, upX = upX, upY = upY,