From 9eec4709bf396f53e6fcbd91b07ce13d5f9d9b77 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 7 Sep 2026 14:54:17 +0200 Subject: [PATCH] feat: add Morgue.score and Morgue.equipment --- src/morgue.lua | 132 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 src/morgue.lua diff --git a/src/morgue.lua b/src/morgue.lua new file mode 100644 index 0000000..aa7adce --- /dev/null +++ b/src/morgue.lua @@ -0,0 +1,132 @@ +-- End-of-run obituary. Turns a finished game's own stats, gear and message +-- log into a NetHack-style morgue report plus a single comparable score, +-- reusing exactly the field names Game:metaStats/graveEntry already expose +-- so a future stat only has to land there to show up here too. + +local Curses = require("curses") +local Compendium = require("compendium") +local Titles = require("titles") + +local Morgue = {} + +Morgue.SCORE_WEIGHTS = { + depth = 100, kill = 10, bossKill = 250, quest = 150, artifact = 200, + vault = 120, setPiece = 180, chest = 15, playerLevel = 50, win = 1000, +} + +-- A single deterministic number from a Morgue.build report, so two runs +-- (or a run against its own best-so-far) can be ranked without re-reading +-- every field by hand. +function Morgue.score(report) + local w = Morgue.SCORE_WEIGHTS + local total = report.deepestDepth * w.depth + report.kills * w.kill + + report.bossKills * w.bossKill + report.questsCompleted * w.quest + + report.artifactsFound * w.artifact + report.vaultsOpened * w.vault + + report.setPiecesFound * w.setPiece + report.chestsOpened * w.chest + + report.playerLevel * w.playerLevel + report.gold + if report.won then total = total + w.win end + return total +end + +Morgue.EQUIP_SLOTS = { "weapon", "armor", "ring", "bow" } + +-- One line per worn slot, empty-handed slots omitted outright rather than +-- padded with "(nothing)" - an obituary should read like a eulogy, not a +-- character sheet. +function Morgue.equipment(player) + local lines = {} + for _, slot in ipairs(Morgue.EQUIP_SLOTS) do + local item = player[slot] + if item then + lines[#lines + 1] = slot .. ": " .. Curses.label(item, item.name) + end + end + return lines +end + +-- Every species this compendium has ever seen the player slay, heaviest +-- kill count first, ties broken by catalog order (Compendium.entries' own +-- fixed order) rather than name so the listing never depends on whatever +-- order table.sort happens to leave equal keys in. +function Morgue.killTally(compendium) + local rows = {} + for i, entry in ipairs(Compendium.entries(compendium)) do + if entry.kills > 0 then + rows[#rows + 1] = { key = entry.key, name = entry.name, kills = entry.kills, order = i } + end + end + table.sort(rows, function(a, b) + if a.kills ~= b.kills then return a.kills > b.kills end + return a.order < b.order + end) + return rows +end + +-- Gathers everything Morgue.format needs straight off a live or just-ended +-- game, mirroring Game:metaStats' own field names so the two never drift. +-- `meta` is optional (see warren.lua's --meta flag) - a meta-free run's +-- report simply carries no title, exactly like it banks no renown. +function Morgue.build(game, meta) + return { + won = game.won, + alive = game.player:isAlive(), + className = game.player.class or "wanderer", + title = meta and Titles.forRenown(meta.renown) or nil, + playerLevel = game.player.level, + deepestDepth = game.deepestDepth, + turn = game.turn, + gold = game.player.gold, + essence = game.player.essence, + kills = game.stats.kills, + bossKills = game.stats.bossKills, + questsCompleted = game.stats.questsCompleted, + artifactsFound = game.stats.artifactsFound, + vaultsOpened = game.stats.vaultsOpened, + setPiecesFound = game.stats.setPiecesFound, + chestsOpened = game.stats.chestsOpened, + equipment = Morgue.equipment(game.player), + killTally = Morgue.killTally(game.compendium), + finalMessages = game.log:recent(5), + } +end + +local function outcomeLine(report) + if report.won then return "escaped the warren with the Amulet" end + if report.alive then return "still drawing breath, for now" end + return "died in the dark" +end + +-- Renders `report` (see Morgue.build) as a fixed-shape text obituary: +-- outcome, vitals, gear, kills, final words, score - always in that order +-- so two runs diff cleanly against each other. +function Morgue.format(report) + local lines = {} + local heading = report.alive and (report.won and "Victorious" or "Wandering") or "Late" + if report.title then + lines[#lines + 1] = string.format("%s the %s, the %s", heading, report.className, report.title) + else + lines[#lines + 1] = string.format("%s the %s", heading, report.className) + end + lines[#lines + 1] = outcomeLine(report) + lines[#lines + 1] = string.format("depth %d, turn %d, level %d", report.deepestDepth, report.turn, + report.playerLevel) + lines[#lines + 1] = string.format("gold %d, essence %d", report.gold, report.essence) + if #report.equipment > 0 then + lines[#lines + 1] = "equipment:" + for _, line in ipairs(report.equipment) do lines[#lines + 1] = " " .. line end + end + if #report.killTally > 0 then + lines[#lines + 1] = "kills:" + for _, row in ipairs(report.killTally) do + lines[#lines + 1] = string.format(" %-14s x%d", row.name, row.kills) + end + end + if #report.finalMessages > 0 then + lines[#lines + 1] = "final moments:" + for _, line in ipairs(report.finalMessages) do lines[#lines + 1] = " " .. line end + end + lines[#lines + 1] = string.format("score: %d", Morgue.score(report)) + return table.concat(lines, "\n") .. "\n" +end + +return Morgue