Files
alchemist-accessibility/accessibility/states/journal.lua
T

161 lines
5.4 KiB
Lua
Raw Normal View History

-- states/journal.lua — quests, lore, bestiary, components, recipes, tutorials.
2026-08-01 22:00:52 +02:00
--
-- Visually the screen is three things at once:
2026-08-01 22:00:52 +02:00
--
-- * a row of TABS across the top (Quests / Lore / Bestiary / Components /
-- Recipes / Tutorials), changed with tabLeft / tabRight;
-- * a scrolling LIST of entries down the left, each an icon plus a title,
-- changed with up / down;
-- * the selected entry's TEXT filling the right-hand page, paginated, with a
-- "< 2 / 3 >" counter under it and left / right flipping pages.
--
-- Only the first two were ever announced, so selecting an entry read its title
-- and then nothing — the page holding the actual content was silent, and page
-- flipping was silent too. That is the whole point of the screen.
--
-- The page is `options[selectedOption].pages[selectedPageId]`, a list of text
-- blocks (`{text = ..., color = ..., icon = ...}`). Its length is bounded by
-- what fits on screen, so a page is safe to speak in one go; it is also pushed
-- into the console history so [ and ] can walk it back, the same trick
-- _textscreen.lua uses for the manual.
--
-- Uses getAnyUpvalue because activeTabIndex, options, selectedOption and
-- selectedPageId are touched by different functions here (gotoTab,
-- prepareOptions, selectPage, keypressed).
local history = require("accessibility.history")
2026-08-01 22:00:52 +02:00
local lastTab
local lastEntry
local lastPushedPage
local lastKey
local lastText
-- The page splitter concatenates the game's own wrapped lines with "\n", so a
-- block's text carries hard newlines that are pure layout. Flatten them, or the
-- screen reader pauses mid-sentence at every wrap point.
local function flatten(text)
return (tostring(text):gsub("%s*\n%s*", " "):gsub("^%s+", ""):gsub("%s+$", ""))
end
local function pageBlocks(page)
local blocks = {}
for _, textBlock in ipairs(page) do
if type(textBlock) == "table" and type(textBlock.text) == "string" then
local text = flatten(textBlock.text)
if text ~= "" then
blocks[#blocks + 1] = text
end
end
end
return blocks
end
2026-08-01 22:00:52 +02:00
return {
-- Called by menus.announceScreen each time the journal is opened, so the tab
-- and title lead again rather than being suppressed as "unchanged" from the
-- last visit.
reset = function()
lastTab = nil
lastEntry = nil
lastPushedPage = nil
lastKey = nil
lastText = nil
end,
2026-08-01 22:00:52 +02:00
read = function(stateTable, menus)
local tabIndex = menus.getAnyUpvalue(stateTable, "activeTabIndex")
local tabsList = menus.getAnyUpvalue(stateTable, "tabsList")
local options = menus.getAnyUpvalue(stateTable, "options")
local selected = menus.getAnyUpvalue(stateTable, "selectedOption")
local pageId = menus.getAnyUpvalue(stateTable, "selectedPageId")
2026-08-01 22:00:52 +02:00
if type(tabIndex) ~= "number" or type(tabsList) ~= "table" then
return nil, nil
end
local optionCount = type(options) == "table" and #options or -1
local entry = type(options) == "table" and type(selected) == "number"
and options[selected] or nil
local key = table.concat({
tostring(tabIndex), tostring(selected), tostring(pageId), tostring(optionCount)
}, ":")
-- read() is called from several places for the same focus: the per-frame
-- poll, the post-keypress announce, the "repeat what's focused" key, and
-- the testbed's before/after probes. Only the FIRST read of a given focus
-- may advance the suppression state below -- otherwise a poll would
-- consume the "tab changed" flag and the announcement that follows would
-- come out without its heading. So later reads of the same key replay the
-- text verbatim, which also makes the repeat key say the full thing.
if key == lastKey then
return lastText, key
end
2026-08-01 22:00:52 +02:00
local tab = tabsList[tabIndex]
local tabName = tab and tab.header or ("tab " .. tabIndex)
local parts = {}
-- Lead with the tab only when it just changed, so stepping through a long
-- quest list doesn't repeat "Quests" on every line.
if tabIndex ~= lastTab then
parts[#parts + 1] = tabName
end
if optionCount == 0 then
2026-08-01 22:00:52 +02:00
parts[#parts + 1] = "empty"
elseif entry then
-- Repeating the title on every page flip would bury the page you just
-- turned to, so the title leads only when the entry itself changed.
local entryKey = tostring(tabIndex) .. ":" .. tostring(selected)
if entryKey ~= lastEntry then
parts[#parts + 1] = entry.title or entry.header or entry.name or "?"
parts[#parts + 1] = selected .. " of " .. optionCount
end
local pages = entry.pages
local page = type(pages) == "table" and type(pageId) == "number" and pages[pageId]
if page then
-- Only worth saying which page you are on when there is more than one.
if #pages > 1 then
parts[#parts + 1] = "page " .. pageId .. " of " .. #pages
end
local blocks = pageBlocks(page)
for _, block in ipairs(blocks) do
parts[#parts + 1] = block
end
if #blocks == 0 then
parts[#parts + 1] = "no text"
end
-- Mirror the page into the review buffer so [ and ] can walk it
-- block by block after the utterance has gone by.
local pageKey = entryKey .. ":" .. tostring(pageId)
if pageKey ~= lastPushedPage then
lastPushedPage = pageKey
for _, block in ipairs(blocks) do
pcall(history.append, block, "journal")
end
end
end
lastEntry = entryKey
2026-08-01 22:00:52 +02:00
end
lastTab = tabIndex
lastKey = key
lastText = #parts > 0 and table.concat(parts, ", ") or nil
2026-08-01 22:00:52 +02:00
return lastText, key
2026-08-01 22:00:52 +02:00
end,
}