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
+81 -4
View File
@@ -2,12 +2,12 @@
**Read this first on any handoff.** Living document: update it whenever state changes, before ending a turn.
Last updated: 2026-08-02 · Currently: **Phases 0–5 COMPLETE. In playtesting** —
Last updated: 2026-08-03 · Currently: **Phases 0–5 COMPLETE. In playtesting** —
fixing what Talon hits. Remaining work is polish, mod settings and distribution.
**Game is playable start-to-finish by ear** from main menu through the intro
conversation into gameplay, plus journal, character, actions and inventory —
verified by the testbed, **38/38 checks**.
verified by the testbed, **59/59 checks**.
**Not set up yet, or unsure how the two trees relate?** →
[00-dev-setup.md](00-dev-setup.md).
@@ -152,6 +152,7 @@ and by Talon at the keyboard (wall tones).
| `Ctrl+Home` | Tracker: walk to current instance (the left-click path) |
| `'` | Health and active effects |
| `,` | Current selected action |
| `=` / `Shift+=` | Read details of the focused thing (Shift = keep the name line) |
Game keys worth knowing: **`x`** enters examine mode (not `e`), `f` finds exits
while examining, `j` journal, `i` inventory, `c` character, `a` actions menu.
@@ -612,6 +613,77 @@ asserts the two upvalue names still resolve — the version-bump canary for this
descriptor. **38/38 checks.** The live path (an actual torch, actually equipped)
is Talon's to confirm.
### Done — item details: `=` and the settle timer (Talon request)
**Symptom:** on the item grids you could hear *which* item you were on
(`a torch, slot 3`) but nothing about what it was — no description, no condition,
and crucially no "what does this refine into", which is the whole of alchemy.
Talon proposed a key. Two findings changed the shape of the answer.
**1. The game already composes the whole detail text in one place.**
`item.getPopupMessage()` (`modules/item.lua:199`) returns the popup's line list:
name+quantity, category, condition (only when `canDegrade`), the free-text
description, a `Yields:` block with one line per refinement type, and
lore-fragment progress. It is a plain method on the item, callable any time — so
`accessibility/itemdetails.lua` reads it rather than re-deriving anything from
templates. Verified text:
`It is a raw material. Some fresh green leaves from ivy. They contain weak
poison. Yields: x1 when ground in a mortar. x0.2 when distilled.`
**2. The game gates this behind a dwell, and so do we.** The popup only fades in
after `config.get("examinationPopupFadeInDuration")` (default 0.4 s). Arrow past
a slot quickly and a *sighted* player never sees the details either. So details
**settle**: stop for `details.config.settleDelay` (0.6 s — longer than 0.4
because speech is slower than a glance) and they speak themselves, queued not
interrupting. `=` reads them on demand; `Shift+=` keeps the name line too.
This mirrors an existing game behaviour instead of inventing a modality.
⚠ **`r` is unusable** even though it was the obvious mnemonic — it is in the
game-claimed scancode list, and `keybindings.getCommand()` dispatches on
scancode ignoring modifiers, so any binding on it can fire a game command. `=`
is from the verified free list and sits by the `[ ] \` review cluster.
**New generic hook: `descriptor.details(stateTable, menus, verbose)`**, returning
a **list of blocks**, not one string. Blocks are spoken joined *and* pushed
individually into the console history, so `[` / `]` / `\` walk a long
description back afterwards — the same trick `states/journal.lua` uses for pages.
Driven by `accessibility/details.lua` (`get` / `speak` / `update` / the pure
`settleStep`). Implemented so far only by `_itemgrid.lua`; `research`,
`ingame_observe` and `journal` are the obvious cheap follow-ups.
**The hint panels are the other half, and one reader covers all three.**
craft / refine / trade each draw a permanent panel beside the grid that the
cursor popup never contains — and **price exists only there**
(`states/trade.lua:156`), as do a recipe's component costs. The three panels turn
out to share a shape (`hintDescription`, a `componentHints` list of
`{nameText, descriptionText, color}`, and a budget line), so `hintBlocks()` in
`_itemgrid.lua` reads all three by upvalue rather than needing per-state
callbacks; unused fields are simply nil. **Affordability is carried only by the
red `{0.9,0.2,0.2}` colour**, and it decides whether enter does anything at all,
so it is decoded from the colour and spoken as `cannot afford`.
Notes:
- The action menu is deliberately **not** excluded from `details` the way it is
from `read()`. The menu is modal so the *verb* is what focus announces, but the
thing you want described is still the item — which is exactly when you want to
know what "Drink" would be drinking.
- The settle key is state-qualified and fires **once per focus key**; leaving a
screen and returning resets, or the same slot would be "already spoken".
- `patches/popups.lua` still stands down on all four grids. That is now
correct-by-design rather than a gap: `details` is the sanctioned route to that
same content, on demand and with history review.
**59 testbed checks, 0 failures** (was 38). New: a synthetic `ivy_leaves` item
asserting description + `Yields` survive (the version-bump canary for
`getPopupMessage`), a `hasUpvalue` canary over all the hint-panel locals in
craft/refine/trade, four assertions on the pure `settleStep`, and an end-to-end
`=` press in the inventory (`No details` on an empty slot — silence would be
indistinguishable from a dropped key).
⚠ **Not exercised against a real item.** Same bucket as the action menu: the
prologue inventory is empty and craft/refine/trade need an alchemy table. The
live path is Talon's to confirm.
### Known gaps (next up)
- **Phase 5**: player status key (HP, statuses, current action), popup
announcements (`popupStack.add`), damage alerts
@@ -676,7 +748,7 @@ utterance — speech is verifiable from the log, not only by ear. `!` after
## HANDOFF — read this if you are resuming
### State of play
Phases 0–5 complete. **23 state descriptors, 6 patches, 38/38 testbed checks**,
Phases 0–5 complete. **23 state descriptors, 6 patches, 59/59 testbed checks**,
history unit tests passing, **one line changed in the game** (`main.lua`).
Everything in the original brief is implemented: console speech with
@@ -686,7 +758,7 @@ exploration, wall tones, and keyboard+speech access to every screen.
### First thing to do after any change
```
lua accessibility/tests/test_history.lua # pure Lua, instant
bash accessibility/tests/run-testbed.sh # ~4 min, 38 checks
bash accessibility/tests/run-testbed.sh # ~4 min, 59 checks
git diff --stat vanilla-0.4.2 -- . ':!accessibility' ':!.gitignore' ':!CLAUDE.md' # must stay: main.lua | 1 +
```
@@ -721,6 +793,11 @@ These are blocked on content the prologue does not contain, **not** known broken
- **Inventory item names, the `equipped` suffix and the item ACTION MENU** —
inventory is empty in the prologue, so only the "empty slot" path has run live.
The action-menu upvalue names are covered by a synthetic testbed check.
- **Item details** (`=`, `Shift+=`, and the 0.6 s settle) against a real item,
and the craft / refine / trade hint panels. The synthetic `ivy_leaves` check
covers `getPopupMessage`, and `hasUpvalue` covers the panel locals, but no
live item has been focused. The settle delay in particular wants tuning by
ear — `details.config.settleDelay`.
- **The quantity slider** (`patches/slider.lua`). Confirmed *applied* by the
self-test but never exercised: `ingame_wait` is unreachable because
`tutorial_bed` has an empty `tryInteractFunction` by design, and