research: read what a node costs, unlocks and does

The research tree spoke only the node's name and whether you could afford
it. Everything that tells you whether it is worth affording -- the
knowledge cost, the recipes it unlocks, the description -- is drawn in
the hint windows beside the tree and was never spoken, so "cannot afford"
gave no way to find out what was missing or by how much.

states/research.lua now implements details(), the same = / Shift+= hook
the item grids use: cost with the shortfall spelled out, what it unlocks,
then the description, one block each so [ and ] walk them back. Shift+=
adds the name line and the whole knowledge purse across all four types.
The focus line also takes its name from the node's title instead of
humanising the id, so it is "Sewing techniques", not "sewing techniques".

Everything is read from the state's currentNode upvalue -- the display
table updateCurrentNode() assembles for the hint window -- rather than
models/research.lua, whose nodes table is file-local and whose unlocks
are recipe ids, which would have named the recipe instead of the item it
produces. The shortfall is computed against journalModel.knowledge,
required lazily because models.journal drags in much of the game.

The tree has no hotkey and the prologue may hold no writing desk, so the
testbed enters it the way the desk does: ingame.interface("research"),
the exact call in the desk's tryInteractFunction. Require it as
"states/ingame" and not "states.ingame" -- states.lua uses the slash
form, and the dot form loads a second copy of the module whose
levelWindow is nil, which research.draw indexes on its first frame.

98 testbed checks, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Accessibility Mod
2026-08-03 13:57:40 +02:00
co-authored by Claude Opus 5
parent a1ad7f26c2
commit ce0f375a10
5 changed files with 279 additions and 15 deletions
+44 -7
View File
@@ -7,7 +7,7 @@ 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, **88/88 checks**.
verified by the testbed, **98/98 checks**.
**Not set up yet, or unsure how the two trees relate?** →
[00-dev-setup.md](00-dev-setup.md).
@@ -443,10 +443,16 @@ Final two descriptors:
selection; `currentlyActiveWindow` says which has focus. Announcing the wrong
panel's row would be actively misleading, as the lists are unrelated.
- **`research`** — a 2D node graph, not a list. Nodes are icons with **no text
label**, so the id is the only identifier; the useful facts are
label**, so the focus line must carry the name; the useful facts are
researched / locked / cannot afford / available. Without the affordability
part the screen is unusable by ear, since enter on an unaffordable node
silently does nothing.
silently does nothing. Name, cost, unlocks and description all come from the
state's **`currentNode`** upvalue — the display table `updateCurrentNode()`
builds for the hint window. Read that rather than `models/research.lua`: its
`nodes` table is file-local, and its `unlocks` are *recipe* ids, whereas
`currentNode.unlocks` is already `{title = <item name>}`. `title` also beats
humanising the id ("Alchemical fire", not "alchemical fire"). Cost, unlocks
and description are served by `details()` on `=`; see below.
**Phase 5 — `accessibility/status.lua`:**
@@ -648,8 +654,10 @@ 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.
`settleStep`). Implemented by `_itemgrid.lua` and by `research` (cost with the
shortfall spelled out, what it unlocks, the description; verbose adds the name
line and the full knowledge purse). `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
@@ -844,6 +852,35 @@ testbed check is this descriptor's version-bump canary**.
Still unexercised: the **map** popup (needs `worldmap.getLastAreaIcon`, i.e. a
discovered area) and the modal **loot** popup from `inventory.lootPopup`.
### Done — research tree details (Talon request)
The research screen read only the node name and whether it was affordable. Cost,
what it unlocks and the description are all drawn in the hint windows beside the
tree and were never spoken, so "cannot afford" gave no way to find out *what* was
missing. `states/research.lua` now implements `details()` — the same `=` /
`Shift+=` hook the item grids use — and the focus line takes its name from
`currentNode.title` instead of humanising the node id.
Everything comes from the state's **`currentNode`** upvalue, the display table
`updateCurrentNode()` assembles for the hint window: `title`, `knowledgeCost`,
`unlocks` (already `{title = <item name>}`), `description`. Not from
`models/research.lua` — its `nodes` table is file-local and its `unlocks` are
*recipe* ids, so it would name the recipe rather than the item. The shortfall is
computed against `journalModel.knowledge`, required lazily.
Verified live: `Sewing techniques, cannot afford` then
`Costs 4 practical knowledge, short 4 practical. Unlocks Protective gloves, Tall
boots. Simple craft of needle and string, …`; verbose adds the name line and
`Knowledge available, 0 practical, 0 ecclesiastic, 0 academic, 0 occult`.
**98 testbed checks, 0 failures** (was 88). The tree has no hotkey and the
prologue may have no writing desk, so the testbed enters it the way the desk
does: `ingame.interface("research")`, the exact call in the desk's
`tryInteractFunction`. ⚠ Require it as **`states/ingame`**, not `states.ingame` —
`states.lua` uses the slash form, and the dot form loads a second copy of the
module whose `levelWindow` is nil, which `research.draw` indexes on its first
frame.
### Known gaps (next up)
- **Phase 5**: player status key (HP, statuses, current action), popup
announcements (`popupStack.add`), damage alerts
@@ -908,7 +945,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. **24 state descriptors, 7 patches, 88/88 testbed checks**,
Phases 0–5 complete. **24 state descriptors, 7 patches, 98/98 testbed checks**,
history unit tests passing, **one line changed in the game** (`main.lua`).
Everything in the original brief is implemented: console speech with
@@ -918,7 +955,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, 88 checks
bash accessibility/tests/run-testbed.sh # ~4 min, 98 checks
git diff --stat vanilla-0.4.2 -- . ':!accessibility' ':!.gitignore' ':!CLAUDE.md' # must stay: main.lua | 1 +
```