items: read the details of the focused item

The item grids said which item you were on ("a torch, slot 3") but nothing
about what it was -- no description, no condition, and no "what does this
refine into", which is the whole of alchemy.

The game already composes all of it in one place: item.getPopupMessage()
(modules/item.lua:199) returns the cursor popup's line list, so read that
rather than re-deriving anything from templates.

It also already gates it behind a dwell -- the popup only fades in after
examinationPopupFadeInDuration (0.4s), so a sighted player who arrows past a
slot never sees the details either. Mirror that instead of inventing a
modality: stop on an item for 0.6s and the details speak themselves, queued.
"=" reads them on demand, Shift+= keeps the name line too. ("r" was the
obvious mnemonic but is a game-claimed scancode, and getCommand() dispatches
on scancode ignoring modifiers.)

New generic hook descriptor.details() returning a LIST of blocks, so each is
also pushed into the console history and [ ] \ walk a long description back
afterwards -- the journal page trick. Only _itemgrid.lua implements it so far.

The other half is the hint panel craft/refine/trade draw permanently beside
the grid, which the popup never contains: recipe components, and price, which
appears nowhere else in the game. The three panels share a shape, so one
reader covers them. Affordability survives only as a red colour in the hint
table and decides whether enter does anything, so it is decoded and spoken.

59 testbed checks, 0 failures: a synthetic ivy_leaves item as the version-bump
canary for getPopupMessage, hasUpvalue over every hint-panel local, four
assertions on the pure settleStep, and an end-to-end "=" press. Not yet
exercised against a live item -- the prologue inventory is empty.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Accessibility Mod
2026-08-03 01:31:58 +02:00
co-authored by Claude Opus 5
parent c841bcc68d
commit a00f327c68
8 changed files with 617 additions and 8 deletions
+89
View File
@@ -0,0 +1,89 @@
-- itemdetails.lua — turn an item into speakable detail blocks.
--
-- The game already composes the whole of an item's detail text in one place:
-- `item.getPopupMessage()` (modules/item.lua:199). It returns a list of
-- `{text =, icon =, font =}` lines that the cursor popup renders:
--
-- a torch. <- name, with (quantity) if stackable
-- It is a tool. <- category, from item/categories.lua
-- It is pretty worn (43% condition).<- only when canDegrade
-- <the free-text description>
-- Yields: <- only when the item can be refined
-- x1 when ground in a mortar. <- one per refinement type
-- Contained knowledge: <- only when containedLore
-- (2/5 fragments learned so far)
--
-- That is exactly what a player needs to evaluate an item, including "what does
-- this turn into", and it is already assembled -- so we read it rather than
-- re-deriving any of it from templates.
--
-- The icons are dropped: every icon line's `text` already names the thing in
-- words ("x1 when ground in a mortar"), so nothing is lost by ear.
--
-- Pure formatting, no LÖVE and no game state, so the testbed can exercise it
-- against a synthetic item.
local itemdetails = {}
-- The game wraps its own text and joins the wrapped lines with "\n", so those
-- newlines are pure layout. Left in, the reader pauses mid-sentence at every
-- wrap point. Same treatment as journal pages (states/journal.lua).
local function flatten(text)
return (tostring(text):gsub("%s*\n%s*", " "):gsub("^%s+", ""):gsub("%s+$", ""))
end
itemdetails.flatten = flatten
-- Does this popup line merely repeat the name the focus announcement just said?
-- getPopupMessage's first line is "<name>." or "<name> (3)." -- speaking it again
-- immediately after "a torch, slot 1" is pure repetition.
local function isNameLine(text, item)
local name = item and item.name
if type(name) ~= "string" then
return false
end
return text:sub(1, #name) == name
end
-- Returns a list of speakable blocks for `item`, or nil if there is nothing to
-- say. `verbose` keeps the leading name line as well.
function itemdetails.blocks(item, verbose)
if type(item) ~= "table" then
return nil
end
local blocks = {}
local ok, lines = pcall(function()
return item.getPopupMessage and item.getPopupMessage()
end)
if ok and type(lines) == "table" then
for index, line in ipairs(lines) do
local text = type(line) == "table" and line.text or (type(line) == "string" and line)
if type(text) == "string" then
text = flatten(text)
if text ~= "" and not (index == 1 and not verbose and isNameLine(text, item)) then
blocks[#blocks + 1] = text
end
end
end
elseif type(item.description) == "string" then
-- getPopupMessage missing or raising means the game changed shape. Degrade
-- to the raw description rather than going silent.
local text = flatten(item.description)
if text ~= "" then
blocks[#blocks + 1] = text
end
end
if #blocks == 0 then
return nil
end
return blocks
end
return itemdetails