feat: add bonusText and kindDetail

This commit is contained in:
dualfroz committed 2026-09-04 22:44:04 +02:00
1 parent cea6315a95
commit 104157745f
1 file changed
+206
+206
View File
@@ -0,0 +1,206 @@
-- Detailed, read-only inspection of a single inventory item or a visible
-- monster. Aggregates facts that several other modules already track (an
-- affix's on-hit rider, a socketed gem's element, a runeword's live bonus,
-- an elite's affix names, a monster's resistances/awareness/status effects)
-- into one readable report - none of which any existing HUD line
-- (render.lua's status panel, commands.lua's own inventory/look) surfaces
-- beyond a bare name. Every function here is read-only: it never mutates
-- the item or monster it describes, and never spends a turn (see
-- commands.lua's own examine handler).
local Items = require("items")
local Identify = require("identify")
local Curses = require("curses")
local Durability = require("durability")
local Runewords = require("runewords")
local Sets = require("sets")
local Shop = require("shop")
local Resistances = require("resistances")
local Factions = require("factions")
local Stealth = require("stealth")
local Disease = require("disease")
local Examine = {}
local AWARENESS_LABEL = {
[Stealth.DORMANT] = "asleep",
[Stealth.SEARCHING] = "searching for you",
[Stealth.HUNTING] = "hunting you",
}
-- Joins whichever of attack/defense/regen a runeword or set threshold bonus
-- carries, in a fixed order, so the same helper reads either shape.
local function bonusText(bonus)
local parts = {}
if bonus.attack and bonus.attack ~= 0 then parts[#parts + 1] = string.format("attack +%d", bonus.attack) end
if bonus.defense and bonus.defense ~= 0 then parts[#parts + 1] = string.format("defense +%d", bonus.defense) end
if bonus.regen and bonus.regen ~= 0 then parts[#parts + 1] = string.format("regen +%d", bonus.regen) end
return table.concat(parts, ", ")
end
local NO_PRICE = { [Items.GOLD] = true, [Items.KEY] = true }
-- Extra facts specific to one gear/consumable kind, appended after the
-- shared label; nil for kinds with nothing beyond their name worth calling
-- out (a plain key, a riftstone shard...).
local function kindDetail(item)
if item.kind == Items.WEAPON or item.kind == Items.ARMOR or item.kind == Items.BOW then
local parts = { string.format("power %d", item.power) }
if item.range then parts[#parts + 1] = "range " .. item.range end
if item.damageType then parts[#parts + 1] = "deals " .. item.damageType .. " damage" end
Durability.ensure(item)
if Durability.isBroken(item) then
parts[#parts + 1] = string.format("durability %d/%d (broken)", item.durability, item.maxDurability)
else
parts[#parts + 1] = string.format("durability %d/%d", item.durability, item.maxDurability)
end
if item.temper and item.temper > 0 then parts[#parts + 1] = string.format("tempered +%d", item.temper) end
return table.concat(parts, ", ")
elseif item.kind == Items.RING then
return string.format("attack +%d, defense +%d", item.attack, item.defense)
elseif item.kind == Items.FOOD then
return string.format("restores %d nutrition", item.nutrition)
elseif item.kind == Items.TOME then
return "teaches the spell: " .. item.spell
elseif item.kind == Items.AMMO then
return string.format("%d in the stack", item.count)
elseif item.kind == Items.CHARM then
return "summons a companion: " .. item.companion
elseif item.kind == Items.KIT then
return "trap effect: " .. item.effect
elseif item.kind == Items.CONTRACT then
return "hires: " .. item.hireling
elseif item.kind == Items.PICK then
return string.format("%d uses left", item.uses)
elseif item.kind == Items.GEM then
return string.format("tier %d, power %d", item.tier, item.power)
end
return nil
end
-- Every non-kind-specific trait a piece of gear may carry, appended in a
-- fixed order regardless of which of them are actually present.
local function gearTraits(item, lines)
if item.affix then
local desc = item.affix.name
if item.affix.onHit then
desc = string.format("%s (on hit: %s, %d turns, potency %d)", desc,
item.affix.onHit.status, item.affix.onHit.duration, item.affix.onHit.potency)
end
lines[#lines + 1] = "affix: " .. desc
end
if item.artifact then
lines[#lines + 1] = "artifact power: " .. (item.unique or "passive bonus")
end
if item.set then
local spec = Sets.byKey[item.set]
lines[#lines + 1] = "set piece: " .. (spec and spec.name or item.set)
end
if item.socket then
lines[#lines + 1] = string.format("socketed: tier %d %s gem", item.socket.tier, item.socket.element)
end
if item.runes and #item.runes > 0 then
local word = Runewords.activeWord(item)
if word then
lines[#lines + 1] = string.format("runeword %s: %s", word.name, bonusText(word))
else
lines[#lines + 1] = "runes: " .. table.concat(item.runes, "-")
end
end
if item.resist then
local parts = {}
for damageType, tier in pairs(item.resist) do
parts[#parts + 1] = damageType .. " " .. tier
end
table.sort(parts)
lines[#lines + 1] = "grants: " .. table.concat(parts, ", ")
end
if Curses.isCursed(item) and Curses.isRevealed(item) then
lines[#lines + 1] = "cursed (" .. item.curseKind .. ")"
end
end
-- A multi-line, semicolon-joined report on a single inventory item: its
-- (possibly still-disguised, see identify.lua) label, then whatever the
-- fields above actually populate. Unidentified potions/scrolls only ever
-- reach the label + value lines, since kindDetail and gearTraits both key
-- off fields those kinds never carry.
function Examine.item(identifyState, item)
local lines = { Curses.label(item, Identify.label(identifyState, item)) }
local detail = kindDetail(item)
if detail then lines[#lines + 1] = detail end
gearTraits(item, lines)
if not NO_PRICE[item.kind] then
lines[#lines + 1] = "worth " .. Shop.priceOf(item) .. " gold"
end
return table.concat(lines, "; ")
end
-- A multi-line, semicolon-joined report on a monster the player can
-- currently see: its combat numbers (already folded through
-- Entity:effectiveAttack/effectiveDefense, so momentum/hunger/sanity
-- modifiers show up exactly as they would land the next swing), its
-- alertness, standing faction and reputation tier, elite/boss tags, any
-- non-normal resistance, and whatever status effects currently afflict it.
function Examine.monster(player, monster)
local lines = { monster.name }
lines[#lines + 1] = string.format("hp %d/%d, attack %d, defense %d",
monster.hp, monster.maxHp, monster:effectiveAttack(), monster:effectiveDefense())
lines[#lines + 1] = "awareness: " .. (AWARENESS_LABEL[monster.awareness] or "hunting you")
if monster.provoked then lines[#lines + 1] = "provoked" end
if monster.faction then
local tier = Factions.tier(Factions.reputationFor(player, monster.faction))
lines[#lines + 1] = string.format("faction: %s (%s)", monster.faction, tier)
end
if monster.elite then
lines[#lines + 1] = (monster.eliteTier or "elite") .. ": " .. table.concat(monster.eliteAffixes or {}, ", ")
end
if monster.isBoss then
local tags = {}
if monster.enraged then tags[#tags + 1] = "enraged" end
if monster.desperate then tags[#tags + 1] = "desperate" end
lines[#lines + 1] = #tags > 0 and ("boss, " .. table.concat(tags, ", ")) or "boss"
end
local resistParts = {}
for _, damageType in ipairs(Resistances.TYPES) do
local tier = Resistances.tierOf(monster, damageType)
if tier ~= Resistances.NORMAL then
resistParts[#resistParts + 1] = damageType .. " " .. tier
end
end
if #resistParts > 0 then lines[#lines + 1] = "resistances: " .. table.concat(resistParts, ", ") end
if monster.statuses and #monster.statuses > 0 then
local parts = {}
for _, active in ipairs(monster.statuses) do
parts[#parts + 1] = string.format("%s (%d)", active.kind, active.duration)
end
lines[#lines + 1] = "afflicted: " .. table.concat(parts, ", ")
end
-- Only reachable via infighting.lua's own bite chance (a rival monster or
-- the player's own companion/hireling/thrall can now catch a disease from
-- a carrier's bite, not just the player) - shown the same way a
-- durability-damaged item's examine line reports a fact only visible
-- through inspection, not the bare glyph on the map.
if Disease.isInfected(monster) then
local def = Disease.defs[monster.disease.key]
lines[#lines + 1] = Disease.hasErupted(monster)
and string.format("diseased: %s (stage %d)", def.name, monster.disease.stage)
or string.format("diseased: %s (incubating)", def.name)
end
return table.concat(lines, "; ")
end
return Examine