84 lines
3.9 KiB
Lua
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
|