journal: speak the page text, not just the entry title

Opening the journal and selecting an entry announced its title and stopped,
which is an index into content the player could never reach. Page flipping was
silent too.

The screen is three widgets at once: the tab row (q/e), the list of entry
titles down the left (up/down), and the page on the right holding the actual
text (left/right). The descriptor read only the first two.

It now reads options[selectedOption].pages[selectedPageId] and speaks its text
blocks. A page is bounded by what fits on screen, so it is safe as a single
utterance. Layout newlines are flattened first -- splitTextByPages joins the
game's own wrapped lines with \n, so left in they make the reader pause
mid-sentence at every wrap point. Page position is announced only when there is
more than one page, and the blocks are pushed into the console history so [ and
] walk back through a long lore entry after the utterance has gone by.

The tab leads only when the tab changed and the title only when the entry
changed, so flipping pages does not bury the new page under a repeated heading.

That suppression made read() stateful, which is a trap: it is called for the
same focus from four places -- the per-frame poll, the post-keypress announce,
repeatFocus, and the testbed's before/after probes -- so a poll would consume
the "changed" flag and the real announcement would come out headless. The
descriptor now caches its last (key, text) and replays it whenever the key is
unchanged, making read() idempotent; the suppression state advances exactly
once per genuine focus change.

Adds a generic descriptor.reset hook, called from menus.announceScreen. The
memory lives in file-locals that outlive the screen, so without it reopening
the journal on the entry you left it on would read the page with no title.

Testbed asserts the spoken text contains the page body rather than merely being
non-nil. 29 checks, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Accessibility Mod
2026-08-02 01:04:03 +02:00
co-authored by Claude Opus 5
parent 81a209b600
commit e1d9995c9f
5 changed files with 236 additions and 21 deletions
+35 -1
View File
@@ -435,12 +435,42 @@ local function scenario()
end
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"
-- with no way to find out what it says, so assert the body specifically.
local function checkJournalReadsPage()
local spoken = focusText()
local stateTable = menus.getStateTable("journal")
local options = stateTable and menus.getAnyUpvalue(stateTable, "options")
local selected = stateTable and menus.getAnyUpvalue(stateTable, "selectedOption")
local pageId = stateTable and menus.getAnyUpvalue(stateTable, "selectedPageId")
local entry = type(options) == "table" and type(selected) == "number" and options[selected]
local page = entry and type(entry.pages) == "table" and entry.pages[pageId]
local block = page and page[1] and page[1].text
if not block then
print("[testbed] journal: no page text to compare against (skipped)")
return
end
-- Compare on the first few words only: the descriptor flattens the
-- layout newlines the page splitter inserts, so the strings are not
-- byte-identical by design.
local opening = block:gsub("%s+", " "):gsub("^%s+", ""):sub(1, 30)
check("journal speaks the page body, not just the title",
spoken ~= nil and spoken:find(opening, 1, true) ~= nil,
"expected to contain " .. string.format("%q", opening) ..
", spoken=" .. tostring(spoken))
end
-- Phase 4: the menu screens reachable from gameplay.
--
-- Each is opened, navigated once, and closed. `back` is bound to x/escape/
-- backspace, but x also opens examine mode, so use escape.
local screens = {
{key = "j", state = "journal", nav = {"down", "right"}},
{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"}},
@@ -460,6 +490,10 @@ local function scenario()
check(screen.state .. " describes focus", focusText() ~= nil,
"focus=" .. tostring(focusText()))
if screen.check then
screen.check()
end
press("escape", "close " .. screen.state)
wait(15)
end