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
+84 -1
View File
@@ -464,6 +464,71 @@ local function scenario()
end
end
-- Item DETAILS (the `=` key and the settle timer). Same problem as the
-- action menu above: the prologue inventory is empty, so there is no real
-- item to focus. The fragile parts are (a) item.getPopupMessage still
-- existing and still carrying the description and the refine yields, and
-- (b) the hint-panel locals in craft/refine/trade keeping their names.
-- Both are checkable without a live screen.
do
local itemdetails = require("accessibility.itemdetails")
local newItem = require("modules.item")
-- ivy_leaves has a description AND both a grind and a distill recipe,
-- so one item exercises every branch of getPopupMessage worth having.
local item = newItem{templateId = "ivy_leaves", quantity = 3}
local blocks = itemdetails.blocks(item)
local joined = table.concat(blocks or {"<nil>"}, " | ")
check("item details produce blocks", type(blocks) == "table" and #blocks > 0,
"blocks=" .. joined)
check("item details include the description",
joined:find("weak poison", 1, true) ~= nil, joined)
check("item details include the refinement yields",
joined:find("Yields", 1, true) ~= nil
and joined:find("mortar", 1, true) ~= nil, joined)
check("item details drop the name line already spoken by read()",
blocks ~= nil and blocks[1] ~= nil
and blocks[1]:find("bunch of ivy", 1, true) == nil,
tostring(blocks and blocks[1]))
check("verbose item details keep the name line",
(itemdetails.blocks(item, true) or {""})[1]:find("ivy", 1, true) ~= nil)
-- Hint-panel canary. These locals hold the recipe cost, the refinement
-- output and (in trade) the price, none of which appear in the cursor
-- popup -- a rename would make them silently vanish.
local hintLocals = {
craft = {"hintDescription", "componentHints"},
refine = {"hintDescription", "outputName", "outputQuantityText",
"budgetNameText", "budgetQuantityText"},
trade = {"componentHints", "budgetNameText", "budgetQuantityText"},
}
for stateName, names in pairs(hintLocals) do
local stateTable = menus.getStateTable(stateName)
for _, name in ipairs(names) do
check(stateName .. " hint panel still has local '" .. name .. "'",
stateTable ~= nil and menus.hasUpvalue(stateTable, name))
end
end
-- The settle timer: due exactly once per focus key, and re-armed by a
-- change. Without "exactly once" a stationary cursor would re-speak the
-- description every frame.
local details = require("accessibility.details")
details.reset()
check("settle does not fire before the delay",
details.settleStep("a", 0.1) == false)
check("settle fires once the delay has passed",
details.settleStep("a", details.config.settleDelay) == true)
check("settle does not fire twice for the same focus",
details.settleStep("a", 10) == false)
check("a focus change re-arms the settle",
details.settleStep("b", 0.01) == false
and details.settleStep("b", 10) == true)
details.reset()
end
-- The journal's whole point is the page of text on the right-hand side; the
-- list of titles on the left is just an index into it. Announcing only the
-- title (which is what it used to do) reads as "there is an entry here"
@@ -494,6 +559,24 @@ local function scenario()
", spoken=" .. tostring(spoken))
end
-- The `=` key end to end. The prologue inventory is empty, so what this
-- proves is the wiring: the key reaches details.speak, is consumed rather
-- than falling through to the game, and an empty slot says "No details"
-- instead of nothing at all (silence being indistinguishable from a
-- dropped keypress). The text itself is covered by the synthetic item above.
local function checkDetailsKey()
local details = require("accessibility.details")
check("empty slot has no details",
details.get("inventory") == nil)
press("=", "read item details")
wait(5)
check("the details key did not leave the inventory",
currentState() == "inventory", "state is " .. tostring(currentState()))
end
-- Phase 4: the menu screens reachable from gameplay.
--
-- Each is opened, navigated once, and closed. `back` is bound to x/escape/
@@ -502,7 +585,7 @@ local function scenario()
{key = "j", state = "journal", nav = {"down", "right"}, check = checkJournalReadsPage},
{key = "c", state = "character", nav = {"right", "right"}},
{key = "a", state = "actions_menu", nav = {"down"}},
{key = "i", state = "inventory", nav = {"right", "down"}},
{key = "i", state = "inventory", nav = {"right", "down"}, check = checkDetailsKey},
}
for _, screen in ipairs(screens) do