feat: extend game with Game.new and Game:metaStats

This commit is contained in:
dualfroz committed 2026-09-06 11:51:48 +02:00
1 parent e188355386
commit 2d1cf09afa
1 file changed
+185 -4
+185 -4
View File
@@ -10,51 +10,232 @@ local Los = require("los")
local Light = require("light") local Light = require("light")
local Combat = require("combat") local Combat = require("combat")
local Items = require("items") local Items = require("items")
local Shop = require("shop")
local Effects = require("effects") local Effects = require("effects")
local Status = require("status") local Status = require("status")
local Identify = require("identify") local Identify = require("identify")
local Builder = require("builder")
local Dungeon = require("dungeon") local Dungeon = require("dungeon")
local Entity = require("entity") local Entity = require("entity")
local MessageLog = require("messagelog") local MessageLog = require("messagelog")
local Scheduler = require("scheduler") local Scheduler = require("scheduler")
local Ai = require("ai") local Ai = require("ai")
local Serialize = require("serialize") local SaveGame = require("savegame")
local Render = require("render") local Render = require("render")
local Progression = require("progression")
local Monsters = require("monsters")
local Spells = require("spells")
local Throwing = require("throwing")
local Ranged = require("ranged")
local Abilities = require("abilities")
local Hazards = require("hazards")
local Terrain = require("terrain")
local Shrines = require("shrines")
local Classes = require("classes")
local Skills = require("skills")
local Crafting = require("crafting")
local Companions = require("companions")
local Mercenaries = require("mercenaries")
local Artifacts = require("artifacts")
local Sets = require("sets")
local Hunger = require("hunger")
local Mimics = require("mimics")
local Bosses = require("bosses")
local Phases = require("phases")
local Quests = require("quests")
local Meta = require("meta")
local Alchemy = require("alchemy")
local Resistances = require("resistances")
local Factions = require("factions")
local Encumbrance = require("encumbrance")
local Elites = require("elites")
local Vaults = require("vaults")
local Runewords = require("runewords")
local Atmosphere = require("atmosphere")
local Events = require("events")
local Stealth = require("stealth")
local Compendium = require("compendium")
local Nodes = require("nodes")
local Curses = require("curses")
local Momentum = require("momentum")
local Corpses = require("corpses")
local Digging = require("digging")
local Sanity = require("sanity")
local Graves = require("graves")
local Proficiency = require("proficiency")
local Ascension = require("ascension")
local Durability = require("durability")
local Conductivity = require("conductivity")
local Disarm = require("disarm")
local Reanimation = require("reanimation")
local Secrets = require("secrets")
local Chests = require("chests")
local Nemesis = require("nemesis")
local Scavenging = require("scavenging")
local Infighting = require("infighting")
local Blocks = require("blocks")
local Noise = require("noise")
local Disease = require("disease")
local Captives = require("captives")
local Level = require("level")
local Horde = require("horde")
local Branches = require("branches")
local Commands = require("commands")
-- Real feature history, oldest first, matching the fields Game.fromData
-- below has always had to guard with "or" because they arrived after the
-- original snapshot shape: essence and the vault/set-piece tallies, then the
-- skill tree and reputation, then sanity. A run save from before any of
-- those landed is missing exactly the fields each step below fills in.
local GameSave = SaveGame.new({
[1] = function(data)
data.playerExtra.essence = data.playerExtra.essence or 0
data.stats = data.stats or { kills = 0, bossKills = 0, questsCompleted = 0, artifactsFound = 0 }
data.stats.vaultsOpened = data.stats.vaultsOpened or 0
data.stats.setPiecesFound = data.stats.setPiecesFound or 0
return data
end,
[2] = function(data)
data.playerExtra.skills = data.playerExtra.skills or {}
data.playerExtra.skillPoints = data.playerExtra.skillPoints or 0
data.playerExtra.skillCooldowns = data.playerExtra.skillCooldowns or {}
data.playerExtra.reputation = data.playerExtra.reputation or {}
return data
end,
[3] = function(data)
data.playerExtra.sanity = data.playerExtra.sanity or Sanity.MAX
return data
end,
})
local Game = {} local Game = {}
Game.__index = Game Game.__index = Game
local FOV_RADIUS = 8 local FOV_RADIUS = 8
-- Which equip slot each wearable item kind occupies, used to check a slot
-- for a bound curse before letting a new piece take its place.
local slotByItemKind = {
[Items.WEAPON] = "weapon", [Items.ARMOR] = "armor",
[Items.RING] = "ring", [Items.BOW] = "bow",
}
local function sign(n) local function sign(n)
if n > 0 then return 1 elseif n < 0 then return -1 else return 0 end if n > 0 then return 1 elseif n < 0 then return -1 else return 0 end
end end
function Game.new(seed) function Game.new(seed, options)
local game = setmetatable({}, Game) local game = setmetatable({}, Game)
game.seed = seed game.seed = seed
game.rng = Rng.new(seed) game.rng = Rng.new(seed)
game.depth = 1 game.depth = 1
game.turn = 0 game.turn = 0
game.won = false
game.log = MessageLog.new(32) game.log = MessageLog.new(32)
game.identify = Identify.new(game.rng) game.identify = Identify.new(game.rng)
game.dungeon = Dungeon.new(seed) game.dungeon = Dungeon.new(seed, options)
game.maxDepth = Builder.maxDepth(options)
game.level = game.dungeon:get(1) game.level = game.dungeon:get(1)
game.player = Entity.new({ game.player = Entity.new({
id = 0, x = game.level.upX, y = game.level.upY, id = 0, x = game.level.upX, y = game.level.upY,
glyph = "@", name = "you", kind = "player", glyph = "@", name = "you", kind = "player",
hp = 30, maxHp = 30, attack = 5, defense = 1, speed = 100, hp = 30, maxHp = 30, attack = 5, defense = 1, speed = 100,
}) })
game.player.satiation = 1000 game.player.satiation = Hunger.START
game.player.sanity = Sanity.MAX
game.player.gold = 20
game.player.essence = 0
game.player.light = { fuel = 400, radius = 8 } game.player.light = { fuel = 400, radius = 8 }
game.player.level = 1
game.player.xp = 0
game.player.xpNext = Progression.xpToNext(1)
game.player.mana = 10
game.player.maxMana = 10
game.player.spellsKnown = { "missile" }
game.player.inventory = { game.player.inventory = {
Items.potion(10), Items.potion(10), Items.food("ration", 400), Items.scroll("mapping"), Items.potion(10), Items.potion(10), Items.food("ration", 400), Items.scroll("mapping"),
} }
game.player.skills = {}
game.player.skillPoints = 1
game.player.skillCooldowns = {}
game.player.reputation = {}
game.deepestDepth = 1
game.stats = {
kills = 0, bossKills = 0, questsCompleted = 0, artifactsFound = 0, vaultsOpened = 0,
setPiecesFound = 0, chestsOpened = 0,
}
if options and options.meta then
Meta.applyAll(options.meta, game.player)
-- Reuses options.meta's own table (rather than copying it) so every
-- sighting/kill recorded mid-run is already sitting in the table
-- warren.lua writes back out with Meta.save at the end of the run.
options.meta.compendium = options.meta.compendium or Compendium.new()
game.compendium = options.meta.compendium
else
game.compendium = Compendium.new()
end
if options and options.class and Classes.isValid(options.class) then
Classes.apply(game.player, options.class)
end
game.ascension = Ascension.resolve(options and options.meta, options and options.mutators)
Ascension.applyToPlayer(game.ascension, game.player)
game.visible = {} game.visible = {}
game.monsterSeq = 0
game.nemesis = nil
game:computeVisible() game:computeVisible()
return game return game
end end
-- Snapshot fed to meta.lua at the end of a run (see warren.lua's --meta
-- flag): everything Meta.renownFor and every achievement condition read,
-- gathered in one place so neither has to know Game's own field names.
function Game:metaStats()
return {
deepestDepth = self.deepestDepth,
kills = self.stats.kills,
bossKills = self.stats.bossKills,
questsCompleted = self.stats.questsCompleted,
artifactsFound = self.stats.artifactsFound,
vaultsOpened = self.stats.vaultsOpened,
setPiecesFound = self.stats.setPiecesFound,
chestsOpened = self.stats.chestsOpened,
won = self.won,
mutators = self.ascension,
}
end
-- The grave this run leaves behind for a future one to stumble across (see
-- graves.lua and Builder's options.graves): only warren.lua's --meta path
-- ever calls this, and only once a run has actually ended in death -
-- winning walks out with the Amulet, not a grave. className falls back to
-- "wanderer" for a run that never picked a class at all.
function Game:graveEntry()
return {
className = self.player.class or "wanderer",
depth = self.deepestDepth,
gold = self.player.gold,
essence = self.player.essence,
turn = self.turn,
}
end
-- Fresh id for a monster spawned mid-game (summon, split), disjoint from the
-- small sequential ids the builder hands out at level generation.
function Game:nextMonsterId()
self.monsterSeq = self.monsterSeq + 1
return 100000 + self.monsterSeq
end
function Game:spawnMinion(speciesKey, x, y)
local spec = Monsters.byKey[speciesKey]
local minion = Monsters.create(spec, self:nextMonsterId(), x, y, self.depth)
-- Summoned or split mid-fight, so it is already aware rather than asleep -
-- see stealth.lua's header for why builder-placed wild monsters differ.
minion.awareness = Stealth.HUNTING
self.level.entities[#self.level.entities + 1] = minion
return minion
end
function Game:currentEntities() function Game:currentEntities()
return self.level.entities return self.level.entities
end end