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,
|
identify = self.identify,
|
||||||
dungeon = self.dungeon:toData(),
|
dungeon = self.dungeon:toData(),
|
||||||
player = self.player: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(),
|
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
|
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)
|
function Game.fromData(snapshot)
|
||||||
local game = setmetatable({}, Game)
|
local game = setmetatable({}, Game)
|
||||||
game.seed = snapshot.seed
|
game.seed = snapshot.seed
|
||||||
game.depth = snapshot.depth
|
game.depth = snapshot.depth
|
||||||
game.turn = snapshot.turn
|
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.rng = Rng.deserialize(snapshot.rng)
|
||||||
game.identify = snapshot.identify
|
game.identify = snapshot.identify
|
||||||
game.dungeon = Dungeon.fromData(snapshot.dungeon)
|
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 = Entity.fromData(snapshot.player)
|
||||||
game.player.satiation = snapshot.playerExtra.satiation
|
game.player.satiation = snapshot.playerExtra.satiation
|
||||||
|
game.player.sanity = snapshot.playerExtra.sanity
|
||||||
game.player.light = snapshot.playerExtra.light
|
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.log = MessageLog.fromData(snapshot.log)
|
||||||
|
game.compendium = snapshot.compendium or Compendium.new()
|
||||||
|
game.ascension = snapshot.ascension or {}
|
||||||
|
game.nemesis = snapshot.nemesis
|
||||||
game.visible = {}
|
game.visible = {}
|
||||||
game:computeVisible()
|
game:computeVisible()
|
||||||
return game
|
return game
|
||||||
end
|
end
|
||||||
|
|
||||||
function Game:save(path)
|
function Game:save(path)
|
||||||
Serialize.saveFile(path, self:toData())
|
GameSave.save(path, self:toData())
|
||||||
end
|
end
|
||||||
|
|
||||||
function Game.load(path)
|
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
|
end
|
||||||
|
|
||||||
return Game
|
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