-- states/journal.lua — quests, lore, bestiary, components, recipes, tutorials. -- -- Visually the screen is three things at once: -- -- * 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") 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 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, 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") 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 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 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 end lastTab = tabIndex lastKey = key lastText = #parts > 0 and table.concat(parts, ", ") or nil return lastText, key end, }