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
+84
View File
@@ -715,6 +715,90 @@ local function scenario()
end
end
------------------------------------------------------------------
-- The research tree
--
-- Not in the `screens` loop above: it has no hotkey, it is opened by
-- interacting with a writing desk, and the prologue may not contain one.
-- `ingame.interface("research")` is the exact call the desk's
-- tryInteractFunction makes (modules/tile/templates.lua), so this enters
-- the screen the same way the game does -- including handing it the
-- levelWindow it draws the background from.
------------------------------------------------------------------
if currentState() == "ingame" then
-- "states/ingame", not "states.ingame": states.lua requires the slash
-- form, and the dot form is a different package.loaded key, so it
-- would load a SECOND copy of the module whose levelWindow is nil --
-- and research.draw indexes that on its first frame.
local ingameState = require("states/ingame")
pcall(ingameState.interface, "research")
wait(12)
local inResearch = currentState() == "research"
check("writing desk opens the research tree", inResearch,
"state is " .. tostring(currentState()))
if inResearch then
-- The tree opens on node 1,1 with no keypress, so focus is
-- readable immediately.
local focus = focusText()
check("research names the node and its status",
focus ~= nil and focus:find(",") ~= nil,
"focus=" .. tostring(focus))
-- The name must come from currentNode.title, not the id: a
-- humanised id would be lower case and underscore-derived.
check("research speaks a title, not a humanised id",
focus ~= nil and focus:sub(1, 1):match("%u") ~= nil,
"focus=" .. tostring(focus))
local details = require("accessibility.details")
local blocks = details.get("research", false)
local text = blocks and table.concat(blocks, ". ")
check("research has details", blocks ~= nil and #blocks > 0,
"blocks=" .. tostring(blocks and #blocks))
-- Cost is the part that decides whether enter does anything, and
-- the unlock list is why you would pay it. Both are hint-window
-- text that was previously never spoken.
check("research details give the knowledge cost",
text ~= nil and (text:find("Costs", 1, true) ~= nil
or text:find("Researched already", 1, true) ~= nil),
"details=" .. tostring(text))
check("research details say what the node unlocks",
text ~= nil and text:find("Unlocks", 1, true) ~= nil,
"details=" .. tostring(text))
-- Verbose adds the name line and the whole knowledge purse.
local verbose = details.get("research", true)
local verboseText = verbose and table.concat(verbose, ". ")
check("verbose research details add the knowledge available",
verboseText ~= nil
and verboseText:find("Knowledge available", 1, true) ~= nil,
"details=" .. tostring(verboseText))
-- Moving must change what is described; up/down jump rows, so
-- right is the reliable one on row 1.
local before = focusText()
press("right", "research: next node")
check("research describes a different node after moving",
focusText() ~= nil and focusText() ~= before,
"before=" .. tostring(before) .. " after=" .. tostring(focusText()))
press("=", "read research details")
wait(5)
check("the details key did not leave the research tree",
currentState() == "research", "state is " .. tostring(currentState()))
press("escape", "close the research tree")
wait(15)
check("research closes back to the game",
currentState() == "ingame", "state is " .. tostring(currentState()))
end
end
-- The quantity slider (patches/slider.lua) is shared by craft, refine,
-- trade and ingame_wait. Only the last is reachable this early: the
-- tutorial house has a bed, and interacting with one opens the