207 lines
8.4 KiB
Lua
207 lines
8.4 KiB
Lua
-- 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
|