2026-08-02 01:04:03 +02:00
|
|
|
-- states/journal.lua — quests, lore, bestiary, components, recipes, tutorials.
|
2026-08-01 22:00:52 +02:00
|
|
|
--
|
2026-08-02 01:04:03 +02:00
|
|
|
-- Visually the screen is three things at once:
|
2026-08-01 22:00:52 +02:00
|
|
|
--
|
2026-08-02 01:04:03 +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
|
2026-08-02 01:04:03 +02:00
|
|
|
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 {
|
2026-08-02 01:04:03 +02:00
|
|
|
-- 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")
|
2026-08-02 01:04:03 +02:00
|
|
|
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
|
|
|
|
|
|
2026-08-02 01:04:03 +02:00
|
|
|
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
|
|
|
|
|
|
2026-08-02 01:04:03 +02:00
|
|
|
if optionCount == 0 then
|
2026-08-01 22:00:52 +02:00
|
|
|
parts[#parts + 1] = "empty"
|
2026-08-02 01:04:03 +02:00
|
|
|
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
|
|
|
|
|
|
2026-08-02 01:04:03 +02:00
|
|
|
lastTab = tabIndex
|
|
|
|
|
lastKey = key
|
|
|
|
|
lastText = #parts > 0 and table.concat(parts, ", ") or nil
|
2026-08-01 22:00:52 +02:00
|
|
|
|
2026-08-02 01:04:03 +02:00
|
|
|
return lastText, key
|
2026-08-01 22:00:52 +02:00
|
|
|
end,
|
|
|
|
|
}
|