feat: extend render with dormantCount and gearLabel
This commit is contained in:
1 parent
bd949a25a5
commit
63a9fb5f90
2 files changed
+168
-8
No files matched your search
+106
-8
@@ -4,9 +4,25 @@
|
|||||||
|
|
||||||
local Status = require("status")
|
local Status = require("status")
|
||||||
local Identify = require("identify")
|
local Identify = require("identify")
|
||||||
|
local Hazards = require("hazards")
|
||||||
|
local Encumbrance = require("encumbrance")
|
||||||
|
local Stealth = require("stealth")
|
||||||
|
local Nodes = require("nodes")
|
||||||
|
local Sets = require("sets")
|
||||||
|
local Curses = require("curses")
|
||||||
|
local Momentum = require("momentum")
|
||||||
|
local Conductivity = require("conductivity")
|
||||||
|
local Sanity = require("sanity")
|
||||||
|
local Events = require("events")
|
||||||
|
local Proficiency = require("proficiency")
|
||||||
|
local Durability = require("durability")
|
||||||
|
local Hunger = require("hunger")
|
||||||
|
local Disease = require("disease")
|
||||||
|
|
||||||
local Render = {}
|
local Render = {}
|
||||||
|
|
||||||
|
local hazardGlyphs = { [Hazards.FIRE] = "`", [Hazards.GAS] = "," }
|
||||||
|
|
||||||
local function cell(game, x, y)
|
local function cell(game, x, y)
|
||||||
local level = game.level
|
local level = game.level
|
||||||
local map = level.map
|
local map = level.map
|
||||||
@@ -16,36 +32,118 @@ local function cell(game, x, y)
|
|||||||
if game.player.x == x and game.player.y == y then return "@" end
|
if game.player.x == x and game.player.y == y then return "@" end
|
||||||
local monster = level:entityAt(x, y)
|
local monster = level:entityAt(x, y)
|
||||||
if monster then return monster.glyph end
|
if monster then return monster.glyph end
|
||||||
|
local hazard = Hazards.at(level, x, y)
|
||||||
|
if hazard then return hazardGlyphs[hazard.kind] end
|
||||||
|
if level:shrineAt(x, y) then return "_" end
|
||||||
|
if level:blockAt(x, y) then return "0" end
|
||||||
|
if level:plateAt(x, y) then return ";" end
|
||||||
|
if level:cauldronAt(x, y) then return "U" end
|
||||||
|
local node = level:nodeAt(x, y)
|
||||||
|
if node then return Nodes.glyph(node) end
|
||||||
|
local board = level:questBoardAt(x, y)
|
||||||
|
if board then return board.claimed and "=" or "&" end
|
||||||
|
if level:corpseAt(x, y) then return "%" end
|
||||||
|
local chest = level:chestAt(x, y)
|
||||||
|
if chest then return chest.opened and "b" or "B" end
|
||||||
|
if level:riftAt(x, y) then return "Q" end
|
||||||
local here = level:itemsAt(x, y)
|
local here = level:itemsAt(x, y)
|
||||||
if #here > 0 then return here[1].item.glyph end
|
if #here > 0 then return here[1].item.glyph end
|
||||||
end
|
end
|
||||||
return map:get(x, y)
|
return map:get(x, y)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Disease was previously invisible outside the transient log lines
|
||||||
|
-- Disease.tick emits (the eruption, a damage tick, an escalation) - once
|
||||||
|
-- one of those scrolled off, a player mid-incubation or mid-plague had no
|
||||||
|
-- persistent way to check their own affliction at all. Folded into the same
|
||||||
|
-- comma-joined tag line as the timed statuses above, since both are
|
||||||
|
-- "what's currently afflicting this entity" facts the HUD already exists to
|
||||||
|
-- surface.
|
||||||
local function statusTags(entity)
|
local function statusTags(entity)
|
||||||
local tags = {}
|
local tags = {}
|
||||||
for _, active in ipairs(entity.statuses) do
|
for _, active in ipairs(entity.statuses) do
|
||||||
tags[#tags + 1] = active.kind
|
tags[#tags + 1] = active.kind
|
||||||
end
|
end
|
||||||
|
if Disease.isInfected(entity) then
|
||||||
|
local infection = entity.disease
|
||||||
|
local name = Disease.defs[infection.key].name
|
||||||
|
tags[#tags + 1] = Disease.hasErupted(entity)
|
||||||
|
and string.format("%s(stage %d)", name, infection.stage)
|
||||||
|
or string.format("%s(incubating)", name)
|
||||||
|
end
|
||||||
if #tags == 0 then return "none" end
|
if #tags == 0 then return "none" end
|
||||||
return table.concat(tags, ",")
|
return table.concat(tags, ",")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- How many of the level's living monsters are still asleep, out of the total
|
||||||
|
-- - purely informational, so autopilot and other callers never read it.
|
||||||
|
local function dormantCount(level)
|
||||||
|
local count = 0
|
||||||
|
for _, entity in ipairs(level.entities) do
|
||||||
|
if entity.kind == "monster" and entity:isAlive() and Stealth.isUnaware(entity) then
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return count
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Appends a wear indicator to `label` only once `item` has actually taken
|
||||||
|
-- damage - broken outright, or merely dinged - leaving a pristine piece's
|
||||||
|
-- label exactly as clean as before this module existed.
|
||||||
|
local function gearLabel(item, label)
|
||||||
|
if Durability.missing(item) == 0 then return label end
|
||||||
|
if Durability.isBroken(item) then return label .. " (broken)" end
|
||||||
|
return string.format("%s (%d/%d)", label, item.durability, item.maxDurability)
|
||||||
|
end
|
||||||
|
|
||||||
function Render.panel(game)
|
function Render.panel(game)
|
||||||
local player = game.player
|
local player = game.player
|
||||||
local level = game.level
|
local level = game.level
|
||||||
local weapon = player.weapon and Identify.label(game.identify, player.weapon) or "none"
|
local weapon = player.weapon
|
||||||
local armor = player.armor and Identify.label(game.identify, player.armor) or "none"
|
and gearLabel(player.weapon, Curses.label(player.weapon, Identify.label(game.identify, player.weapon))) or "none"
|
||||||
local ring = player.ring and player.ring.name or "none"
|
local armor = player.armor
|
||||||
|
and gearLabel(player.armor, Curses.label(player.armor, Identify.label(game.identify, player.armor))) or "none"
|
||||||
|
local ring = player.ring and Curses.label(player.ring, player.ring.name) or "none"
|
||||||
|
local bow = player.bow and gearLabel(player.bow, Curses.label(player.bow, player.bow.name)) or "none"
|
||||||
local torch = player.light.fuel > 0 and tostring(player.light.fuel) or "spent"
|
local torch = player.light.fuel > 0 and tostring(player.light.fuel) or "spent"
|
||||||
|
local asleep = dormantCount(level)
|
||||||
|
local foes = asleep > 0 and string.format("%d (%d asleep)", #level.entities, asleep)
|
||||||
|
or tostring(#level.entities)
|
||||||
local lines = {
|
local lines = {
|
||||||
string.format("HP %d/%d ATK %d DEF %d depth %d turn %d %s%s",
|
string.format("HP %d/%d ATK %d DEF %d lvl %d depth %d turn %d %s%s%s%s",
|
||||||
player.hp, player.maxHp, player:effectiveAttack(), player:effectiveDefense(),
|
player.hp, player.maxHp, player:effectiveAttack(), player:effectiveDefense(),
|
||||||
game.depth, game.turn, level.generator, level.dark and " dark" or ""),
|
player.level, game.depth, game.turn, level.generator, level.dark and " dark" or "",
|
||||||
string.format("food %d torch %s status %s", player.satiation, torch, statusTags(player)),
|
level.weather and (" " .. level.weather) or "", game.won and " WON" or ""),
|
||||||
string.format("weapon %s armor %s ring %s items %d foes %d",
|
string.format("xp %d/%d mana %d/%d food %d torch %s gold %d status %s",
|
||||||
weapon, armor, ring, #player.inventory, #level.entities),
|
player.xp, player.xpNext, player.mana, player.maxMana, player.satiation, torch, player.gold, statusTags(player)),
|
||||||
|
string.format("load %s (%d/%d)",
|
||||||
|
player:encumbranceTier(), Encumbrance.carriedWeight(player), Encumbrance.capacityFor(player)),
|
||||||
|
string.format("weapon %s armor %s ring %s bow %s items %d foes %s",
|
||||||
|
weapon, armor, ring, bow, #player.inventory, foes),
|
||||||
}
|
}
|
||||||
|
local activeSets = Sets.activeDescriptions(player)
|
||||||
|
if activeSets then lines[#lines + 1] = "set " .. activeSets end
|
||||||
|
local combo = Momentum.stacks(player)
|
||||||
|
if combo > 0 then lines[#lines + 1] = string.format("combo x%d", combo) end
|
||||||
|
if Conductivity.isWet(player) then lines[#lines + 1] = string.format("wet (%d)", player.wet) end
|
||||||
|
local masteryBonus = Proficiency.attackBonus(player)
|
||||||
|
if masteryBonus > 0 then
|
||||||
|
lines[#lines + 1] = string.format("mastery +%d with %s", masteryBonus, Proficiency.keyFor(player))
|
||||||
|
end
|
||||||
|
local sanityTier = Sanity.tierOf(player)
|
||||||
|
if sanityTier ~= Sanity.COMPOSED then
|
||||||
|
lines[#lines + 1] = string.format("sanity %d/%d (%s)", Sanity.current(player), Sanity.MAX, sanityTier)
|
||||||
|
end
|
||||||
|
local hungerTier = Hunger.tierOf(player)
|
||||||
|
if hungerTier ~= Hunger.FED then
|
||||||
|
lines[#lines + 1] = string.format("hunger %d/%d (%s)", Hunger.current(player), Hunger.CAP, hungerTier)
|
||||||
|
end
|
||||||
|
if level.event then
|
||||||
|
lines[#lines + 1] = string.format("event %s (%d turns left)", level.event.key, level.event.turnsLeft)
|
||||||
|
end
|
||||||
|
if game.ascension and #game.ascension > 0 then
|
||||||
|
lines[#lines + 1] = "ascension " .. table.concat(game.ascension, ",")
|
||||||
|
end
|
||||||
local last = game.log:last()
|
local last = game.log:last()
|
||||||
if last then lines[#lines + 1] = "- " .. last end
|
if last then lines[#lines + 1] = "- " .. last end
|
||||||
return table.concat(lines, "\n")
|
return table.concat(lines, "\n")
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
-- Re-simulates a run from nothing but its own seed, options and step count
|
||||||
|
-- to confirm it actually reproduces a recorded outcome - the same guarantee
|
||||||
|
-- rngDeterminism and mapDeterminism already lean on internally, spelled out
|
||||||
|
-- here as a standalone check a caller can run against any (seed, options,
|
||||||
|
-- steps) triple, e.g. to confirm a regression fix left a known-good seed's
|
||||||
|
-- ending untouched, or to flag a leaderboard entry whose recorded report no
|
||||||
|
-- longer matches what that seed actually plays out to.
|
||||||
|
--
|
||||||
|
-- Scope: this replays purely from (seed, options, steps) - it says nothing
|
||||||
|
-- about a run that also carried mutable meta.lua state (perks already
|
||||||
|
-- owned, a compendium already partway filled), since that state moves
|
||||||
|
-- forward with every run and is never itself snapshotted. Call it only for
|
||||||
|
-- a plain run, or pass the exact historical options a meta-bearing run
|
||||||
|
-- actually started with.
|
||||||
|
|
||||||
|
local Game = require("game")
|
||||||
|
local Morgue = require("morgue")
|
||||||
|
|
||||||
|
local Replay = {}
|
||||||
|
|
||||||
|
-- Runs a fresh game for exactly `steps` auto-steps (or until the player
|
||||||
|
-- dies first) and returns its final Morgue.build report.
|
||||||
|
function Replay.run(seed, options, steps)
|
||||||
|
local game = Game.new(seed, options)
|
||||||
|
for _ = 1, steps do
|
||||||
|
if not game.player:isAlive() then break end
|
||||||
|
game:autoStep()
|
||||||
|
end
|
||||||
|
return Morgue.build(game)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Every field Morgue.score actually reads, plus won/alive - deliberately not
|
||||||
|
-- equipment/killTally/finalMessages, which are free-form listings a caller
|
||||||
|
-- rarely wants compared byte-for-byte.
|
||||||
|
Replay.COMPARED_FIELDS = {
|
||||||
|
"won", "alive", "deepestDepth", "turn", "gold", "essence",
|
||||||
|
"kills", "bossKills", "questsCompleted", "artifactsFound",
|
||||||
|
"vaultsOpened", "setPiecesFound", "chestsOpened", "playerLevel",
|
||||||
|
}
|
||||||
|
|
||||||
|
-- True iff `report` and `replayed` agree on every Replay.COMPARED_FIELDS
|
||||||
|
-- entry. Returns (false, field) naming the first mismatch found, in
|
||||||
|
-- Replay.COMPARED_FIELDS order, so a caller can say exactly what diverged
|
||||||
|
-- rather than just that something did.
|
||||||
|
function Replay.matches(report, replayed)
|
||||||
|
for _, field in ipairs(Replay.COMPARED_FIELDS) do
|
||||||
|
if report[field] ~= replayed[field] then return false, field end
|
||||||
|
end
|
||||||
|
return true, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Re-simulates (seed, options, steps) and checks whether it reproduces
|
||||||
|
-- `report` (see Replay.matches). Always returns the score the replay itself
|
||||||
|
-- earned, whether or not it matched, so a caller can tell a merely-outdated
|
||||||
|
-- record from one whose replay actually played worse.
|
||||||
|
function Replay.verify(seed, options, steps, report)
|
||||||
|
local replayed = Replay.run(seed, options, steps)
|
||||||
|
local matches, mismatchField = Replay.matches(report, replayed)
|
||||||
|
return matches, mismatchField, Morgue.score(replayed)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Replay
|
||||||
Reference in new issue
Block a user