Files
warren/src/branches.lua
T

84 lines
3.9 KiB
Lua

-- Rift crossings: a rare, shimmering tear in a floor from depth six on (see
-- builder.lua's placement, gated behind a carried rift shard) that a player
-- can step through into a short, self-contained pocket floor - generated
-- once from the same world seed as everything else (see Branches.build) at
-- a deliberately harsher effective depth than the floor the rift itself sits
-- on, so what waits behind it is always a genuine step up in danger and in
-- reward. Unlike horde.lua's arena (a one-way trial that replaces the run's
-- level outright), a rift crossing is round-trip: Game:enterRift remembers
-- exactly where the player stood before stepping through, and Game:exitRift
-- restores it to the byte, so nothing about the main dungeon left behind is
-- ever perturbed by the detour.
local Rng = require("rng")
local Builder = require("builder")
local Level = require("level")
local Branches = {}
-- How much harder a rift's own pocket floor is generated than the main-track
-- depth its entrance sits on - fixed rather than randomised, so a rift found
-- at a given depth always promises the same order of danger and reward.
Branches.DEPTH_BONUS = 4
-- Distinguishes a rift's own private rng stream from both the main dungeon's
-- per-depth stream (Dungeon:levelSeed's own salt) and every other salted
-- stream in the codebase, so discovering a rift can never perturb - or be
-- perturbed by - any of them.
local SALT = 0xC2B2AE3D27D4EB4F
-- The deterministic seed a rift found on `depth` always builds its pocket
-- floor from, a pure function of the world seed and the origin depth alone -
-- never the main dungeon's own per-depth rng stream - so the exact same rift
-- always opens onto the exact same pocket floor no matter how many times (or
-- in what order) the run's other floors happen to be visited first.
function Branches.seedFor(worldSeed, depth)
return math.tointeger(worldSeed ~ (depth * SALT)) or 0
end
-- Builds the pocket floor a rift found at `depth` opens onto: an ordinary
-- Builder.build floor, just generated Branches.DEPTH_BONUS deeper than the
-- rift's own origin so its monsters and loot are drawn from a rung the
-- player has not reached yet on the main track. `options` is the run's own
-- dungeon options (mutators, class, graves), copied rather than mutated, with
-- maxDepth pinned one short of the pocket's own depth - short, not equal or
-- past it, so neither of Builder.build's two maxDepth-gated passes ever fire
-- in a pocket floor: no chasm (there is deliberately no lower pocket floor to
-- fall into) and, far more importantly, no Amulet of Warren - a rift is a
-- richer fight, never a shortcut past the real dungeon's own win condition.
function Branches.build(worldSeed, depth, options)
local pocketDepth = depth + Branches.DEPTH_BONUS
local rng = Rng.new(Branches.seedFor(worldSeed, depth))
local pocketOptions = {}
for k, v in pairs(options or {}) do pocketOptions[k] = v end
pocketOptions.maxDepth = pocketDepth - 1
return Builder.build(pocketDepth, rng, pocketOptions)
end
-- Every rift's own pocket floor, serialized exactly like Dungeon:toData
-- serializes the main track: a depth-sorted array of {depth, level} pairs, so
-- a save taken mid-crossing (or with a rift merely discovered and never
-- entered) restores every pocket floor already generated byte-for-byte, dead
-- monsters and all - and stores nothing extra for a rift a run never stepped
-- through at all.
function Branches.levelsToData(riftLevels)
local depths = {}
for depth in pairs(riftLevels or {}) do depths[#depths + 1] = depth end
table.sort(depths)
local stored = {}
for _, depth in ipairs(depths) do
stored[#stored + 1] = { depth = depth, level = riftLevels[depth]:toData() }
end
return stored
end
function Branches.levelsFromData(stored)
local riftLevels = {}
for _, entry in ipairs(stored or {}) do
riftLevels[entry.depth] = Level.fromData(entry.level)
end
return riftLevels
end
return Branches