feat: extend game with Graves.record and Graves.eligibleFor
This commit is contained in:
1 parent
5f13d7012e
commit
cb01478890
2 files changed
+183
-4
No files matched your search
+77
-4
@@ -2638,35 +2638,108 @@ function Game:toData()
|
||||
identify = self.identify,
|
||||
dungeon = self.dungeon:toData(),
|
||||
player = self.player:toData(),
|
||||
playerExtra = { satiation = self.player.satiation, light = self.player.light },
|
||||
playerExtra = {
|
||||
satiation = self.player.satiation, sanity = self.player.sanity, light = self.player.light,
|
||||
level = self.player.level, xp = self.player.xp, xpNext = self.player.xpNext,
|
||||
mana = self.player.mana, maxMana = self.player.maxMana, spellsKnown = self.player.spellsKnown,
|
||||
gold = self.player.gold, essence = self.player.essence,
|
||||
class = self.player.class, skills = self.player.skills,
|
||||
skillPoints = self.player.skillPoints, skillCooldowns = self.player.skillCooldowns,
|
||||
quest = self.player.quest, reputation = self.player.reputation,
|
||||
pendingPoisonBlade = self.player.pendingPoisonBlade,
|
||||
},
|
||||
log = self.log:toData(),
|
||||
compendium = self.compendium,
|
||||
ascension = self.ascension,
|
||||
nemesis = self.nemesis,
|
||||
rift = self.rift or false,
|
||||
riftLevels = Branches.levelsToData(self.riftLevels),
|
||||
riftReturn = self.riftReturn and {
|
||||
depth = self.riftReturn.depth, x = self.riftReturn.x, y = self.riftReturn.y,
|
||||
} or nil,
|
||||
-- The arena itself lives outside self.dungeon (see Game:startHorde), so
|
||||
-- without its own snapshot here a mid-arena save would resume from
|
||||
-- self.dungeon:get(self.depth) instead - the ordinary floor the arena
|
||||
-- replaced, abandoned but still cached. horde.wave rides along in the
|
||||
-- same field, since a restored arena with no wave counter would strand
|
||||
-- the player once the surviving wave dies with nothing left to trigger
|
||||
-- the next one.
|
||||
horde = self.horde,
|
||||
hordeLevel = self.horde and self.level:toData() or nil,
|
||||
}
|
||||
end
|
||||
|
||||
-- Expects a complete, current-shape snapshot: every field GameSave's
|
||||
-- migration chain owns (essence, the skill-tree fields, reputation, sanity,
|
||||
-- stats.vaultsOpened/setPiecesFound - see the migrations above) must already
|
||||
-- be present. Game.load guarantees that by running GameSave.load first;
|
||||
-- Game:toData()'s own output always has them too. A handful of unversioned
|
||||
-- fields (won, monsterSeq, deepestDepth, the whole stats table) keep their
|
||||
-- own "or" defaults here since they can be missing outright rather than just
|
||||
-- missing individual leaves, which the migration chain does not model.
|
||||
function Game.fromData(snapshot)
|
||||
local game = setmetatable({}, Game)
|
||||
game.seed = snapshot.seed
|
||||
game.depth = snapshot.depth
|
||||
game.turn = snapshot.turn
|
||||
game.won = snapshot.won or false
|
||||
game.monsterSeq = snapshot.monsterSeq or 0
|
||||
game.deepestDepth = snapshot.deepestDepth or snapshot.depth
|
||||
game.stats = snapshot.stats or {
|
||||
kills = 0, bossKills = 0, questsCompleted = 0, artifactsFound = 0, vaultsOpened = 0,
|
||||
setPiecesFound = 0,
|
||||
}
|
||||
game.rng = Rng.deserialize(snapshot.rng)
|
||||
game.identify = snapshot.identify
|
||||
game.dungeon = Dungeon.fromData(snapshot.dungeon)
|
||||
game.level = game.dungeon:get(game.depth)
|
||||
game.maxDepth = Builder.maxDepth(game.dungeon.options)
|
||||
game.rift = snapshot.rift or false
|
||||
game.riftLevels = Branches.levelsFromData(snapshot.riftLevels)
|
||||
game.riftReturn = snapshot.riftReturn and {
|
||||
depth = snapshot.riftReturn.depth, x = snapshot.riftReturn.x, y = snapshot.riftReturn.y,
|
||||
level = game.dungeon:get(snapshot.riftReturn.depth),
|
||||
} or nil
|
||||
game.horde = snapshot.horde
|
||||
game.level = game.horde and Level.fromData(snapshot.hordeLevel)
|
||||
or (game.rift and game.riftLevels[game.depth] or game.dungeon:get(game.depth))
|
||||
game.player = Entity.fromData(snapshot.player)
|
||||
game.player.satiation = snapshot.playerExtra.satiation
|
||||
game.player.sanity = snapshot.playerExtra.sanity
|
||||
game.player.light = snapshot.playerExtra.light
|
||||
game.player.level = snapshot.playerExtra.level
|
||||
game.player.xp = snapshot.playerExtra.xp
|
||||
game.player.xpNext = snapshot.playerExtra.xpNext
|
||||
game.player.mana = snapshot.playerExtra.mana
|
||||
game.player.maxMana = snapshot.playerExtra.maxMana
|
||||
game.player.spellsKnown = snapshot.playerExtra.spellsKnown
|
||||
game.player.gold = snapshot.playerExtra.gold
|
||||
game.player.essence = snapshot.playerExtra.essence
|
||||
game.player.class = snapshot.playerExtra.class
|
||||
game.player.skills = snapshot.playerExtra.skills
|
||||
game.player.skillPoints = snapshot.playerExtra.skillPoints
|
||||
game.player.skillCooldowns = snapshot.playerExtra.skillCooldowns
|
||||
game.player.quest = snapshot.playerExtra.quest
|
||||
game.player.reputation = snapshot.playerExtra.reputation
|
||||
game.player.pendingPoisonBlade = snapshot.playerExtra.pendingPoisonBlade
|
||||
game.log = MessageLog.fromData(snapshot.log)
|
||||
game.compendium = snapshot.compendium or Compendium.new()
|
||||
game.ascension = snapshot.ascension or {}
|
||||
game.nemesis = snapshot.nemesis
|
||||
game.visible = {}
|
||||
game:computeVisible()
|
||||
return game
|
||||
end
|
||||
|
||||
function Game:save(path)
|
||||
Serialize.saveFile(path, self:toData())
|
||||
GameSave.save(path, self:toData())
|
||||
end
|
||||
|
||||
function Game.load(path)
|
||||
return Game.fromData(Serialize.loadFile(path))
|
||||
local data, err = GameSave.load(path)
|
||||
if not data then
|
||||
error("cannot load save " .. path .. ": " .. err)
|
||||
end
|
||||
return Game.fromData(data)
|
||||
end
|
||||
|
||||
return Game
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
-- Cross-run bones: the moment a run ends in death (see warren.lua's --meta
|
||||
-- path and Game:graveEntry), a grave entry is appended to meta.lua's
|
||||
-- persistent save - what the fallen was still carrying, and how deep they
|
||||
-- got. From the very next run on, Builder.build may roll one of those
|
||||
-- graves onto a floor at or shallower than where it was dug (bones drift
|
||||
-- upward over time, never down - see Graves.eligibleFor) as an inert
|
||||
-- floorItem, drawn dead last of all so a run without any recorded deaths
|
||||
-- (or one played without --meta at all) makes no extra rng draw and its
|
||||
-- floor layouts stay byte-identical to before this module existed.
|
||||
--
|
||||
-- Unlike mimics.lua's disguise, a grave marker is honest about what it is -
|
||||
-- disturbing it (Game:pickupHere, mirroring Game:triggerMimic) wakes a
|
||||
-- vengeful shade rather than pretending to be ordinary loot. The shade's
|
||||
-- hp and attack scale with how much the fallen was carrying at death, and
|
||||
-- felling it in turn pays out a fraction of that same hoard - a richer
|
||||
-- death makes for a tougher fight and a richer reward, but always strictly
|
||||
-- less than what was actually lost.
|
||||
|
||||
local Entity = require("entity")
|
||||
local Factions = require("factions")
|
||||
|
||||
local Graves = {}
|
||||
|
||||
-- Oldest graves fall off once a save accumulates more than this many, so a
|
||||
-- long-lived meta file never grows without bound.
|
||||
Graves.MAX_RECORDED = 20
|
||||
|
||||
-- Appends `entry` (see Game:graveEntry) to meta.graves, most recent first,
|
||||
-- trimming anything past Graves.MAX_RECORDED.
|
||||
function Graves.record(meta, entry)
|
||||
meta.graves = meta.graves or {}
|
||||
table.insert(meta.graves, 1, entry)
|
||||
while #meta.graves > Graves.MAX_RECORDED do
|
||||
table.remove(meta.graves)
|
||||
end
|
||||
end
|
||||
|
||||
-- Graves eligible to be rolled onto `depth`: any dug at that depth or
|
||||
-- deeper. A grave dug on floor 7 can still turn up waiting on floor 4, but
|
||||
-- a floor 2 grave never surfaces on floor 7 - bones drift shallower with
|
||||
-- time, never the other way around.
|
||||
function Graves.eligibleFor(graves, depth)
|
||||
local eligible = {}
|
||||
if not graves then return eligible end
|
||||
for _, grave in ipairs(graves) do
|
||||
if grave.depth >= depth then eligible[#eligible + 1] = grave end
|
||||
end
|
||||
return eligible
|
||||
end
|
||||
|
||||
-- The inert floorItem a builder drops for a rolled grave: visibly a grave
|
||||
-- from the moment it is seen (no disguise to unmask), carrying just enough
|
||||
-- of the fallen's own data for Graves.reveal to build their shade later.
|
||||
function Graves.floorItem(grave)
|
||||
return {
|
||||
kind = "grave", grave = true, name = "a weathered grave marker", glyph = "b",
|
||||
graveClass = grave.className, graveGold = grave.gold, graveEssence = grave.essence,
|
||||
}
|
||||
end
|
||||
|
||||
-- The hostile shade a grave marker becomes the instant it is disturbed:
|
||||
-- hp and attack both scale with the wealth it is guarding, capped so even
|
||||
-- the richest grave never spawns something the player cannot fight back
|
||||
-- against. Mirrors Mimics.reveal's shape (a fresh monster entity, tagged so
|
||||
-- awardKill and corpses.lua both know to treat it specially).
|
||||
function Graves.reveal(id, x, y, ground)
|
||||
local gold = ground.graveGold or 0
|
||||
local essence = ground.graveEssence or 0
|
||||
local wealth = gold + essence * 2
|
||||
local bonusHp = math.min(20, wealth // 8)
|
||||
local bonusAttack = math.min(6, wealth // 30)
|
||||
local entity = Entity.new({
|
||||
id = id, x = x, y = y,
|
||||
glyph = "G", name = "vengeful shade", kind = "monster",
|
||||
species = "shade", ai = "chase",
|
||||
hp = 10 + bonusHp, maxHp = 10 + bonusHp,
|
||||
attack = 4 + bonusAttack, defense = 1, speed = 105,
|
||||
})
|
||||
entity.cooldown = 0
|
||||
entity.faction = Factions.UNDEAD
|
||||
entity.grave = true
|
||||
entity.graveClass = ground.graveClass
|
||||
entity.graveGold = gold
|
||||
entity.graveEssence = essence
|
||||
return entity
|
||||
end
|
||||
|
||||
-- Flat xp reward for felling a revealed shade - kept separate from
|
||||
-- Monsters.xpFor exactly like Mimics.xpFor, since a shade is never a
|
||||
-- member of the wild bestiary Monsters.forDepth draws its per-floor
|
||||
-- population from.
|
||||
function Graves.xpFor()
|
||||
return 12
|
||||
end
|
||||
|
||||
-- What a felled shade actually leaves behind: half the gold and a third of
|
||||
-- the essence the fallen was carrying at the moment of death - the rest is
|
||||
-- gone for good, so a grave is always a net loss across the two runs it
|
||||
-- touches, never a way to launder wealth forward.
|
||||
function Graves.lootFor(entity)
|
||||
local gold = (entity.graveGold or 0) // 2
|
||||
local essence = (entity.graveEssence or 0) // 3
|
||||
return gold, essence
|
||||
end
|
||||
|
||||
return Graves
|
||||
Reference in new issue
Block a user