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>
Game-file footprint unchanged: still one line in main.lua.
All 23 states now have descriptors; 5 patches applied.
Final two descriptors:
- keybindings_menu has two panels (commands and keys) with independent
selection, so it reads currentlyActiveWindow to decide which one has focus.
Announcing the wrong panel's row would be actively misleading, since the two
lists are unrelated.
- research is a 2D node graph rather than a list, and its nodes are drawn as
icons with no text label, so the id is the only identifier available. What
actually matters is researched / locked / cannot afford / available: without
the affordability part the screen is unusable by ear, because pressing enter
on an unaffordable node silently does nothing.
status.lua adds two pull keys -- ' for health and active effects, , for the
current action -- plus unprompted damage alerts.
Damage is polled from actor.hp rather than hooked. ingame.checkInterruptions
exists but only fires for interruptions during queued movement, so damage taken
on your own turn never reaches it. Polling catches every source (combat, poison,
fire, falling) with no game edit. Damage speaks immediately and interrupts;
healing queues, since it is worth knowing but never urgent.
patches/popupstack.lua wraps popupStack.add, the single entry point for loot,
recipe, lore, map, quest, tutorial and bestiary popups. None of these go through
console.print, so they were entirely silent before.
speech.immediate now logs to the console like queued speech does. It bypasses
the frame queue and so also bypassed the flush-time logger, which meant damage
alerts fired correctly but were invisible in the testbed transcript. Prefixes
are now: ! interrupt, * immediate, space queued.
26/26 testbed checks, history unit tests passing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>