-- details.lua — "tell me more about what is focused". -- -- Focus announcements are deliberately terse: "a torch, slot 1". Everything that -- says what an item actually IS -- description, condition, what it refines into, -- what a recipe costs, what it sells for -- is on screen but was never spoken. -- -------------------------------------------------------------------------------- -- Why details settle rather than always speaking -------------------------------------------------------------------------------- -- -- The game itself gates this text behind a dwell: the cursor popup only fades in -- after config.get("examinationPopupFadeInDuration") (default 0.4s). Arrow past a -- slot quickly and a SIGHTED player never sees the details either -- they see them -- by stopping. So we mirror that rather than inventing a modality: stop on a slot -- and the details speak themselves; keep moving and they don't. -- -- `=` reads them on demand and re-reads them, for when you don't want to wait or -- the utterance went by. Shift+= is verbose (keeps the name line too). -- -------------------------------------------------------------------------------- -- The descriptor hook -------------------------------------------------------------------------------- -- -- descriptor.details(stateTable, menus, verbose) -> { "block", "block", ... } -- -- Optional, alongside read/reset. Returning a LIST rather than one string is what -- lets each block be pushed into the console history separately, so [ and ] walk -- the detail text back afterwards -- the same trick states/journal.lua uses for -- journal pages. A long description therefore never has to be caught first time. -- -- Implemented so far by the four item-grid screens (_itemgrid.lua). Any other -- descriptor can gain it for free: research nodes and examine-mode tiles are the -- obvious next ones. local speech = require("accessibility.speech") local history = require("accessibility.history") local menus = require("accessibility.menus") local details = {} details.config = { -- How long focus must stay still before details speak, in seconds. Longer -- than the game's 0.4s popup delay because speech takes longer than a glance, -- and arrowing through a full tab shouldn't leave a backlog behind you. settleDelay = 0.6, -- Set false to make details key-only. autoSpeak = true, } -------------------------------------------------------------------------------- -- Fetching -------------------------------------------------------------------------------- -- Returns the detail blocks for a state, or nil if it has no details to give. function details.get(stateName, verbose) local descriptor = menus.loadDescriptor(stateName) if not descriptor or not descriptor.details then return nil end local stateTable = menus.getStateTable(stateName) if not stateTable then return nil end local ok, blocks = pcall(descriptor.details, stateTable, menus, verbose) if not ok or type(blocks) ~= "table" or #blocks == 0 then return nil end return blocks end -------------------------------------------------------------------------------- -- Speaking -------------------------------------------------------------------------------- local function currentState() local ok, states = pcall(require, "states") if not ok then return nil end return states.getCurrent() end -- Mirror into the review buffer so [ and ] can walk the blocks after the -- utterance has gone by. local function pushToHistory(blocks) for _, block in ipairs(blocks) do pcall(history.append, block, "details") end end local function speakBlocks(blocks, interrupt) pushToHistory(blocks) speech.say(table.concat(blocks, ". "), interrupt) end -- The `=` key. Interrupting, because the player explicitly asked. function details.speak(verbose) local stateName = currentState() local blocks = stateName and details.get(stateName, verbose) if not blocks then -- Never a silent no-op: silence is indistinguishable from a dropped key. speech.say("No details", true) return end speakBlocks(blocks, true) end -------------------------------------------------------------------------------- -- The settle timer -------------------------------------------------------------------------------- local settledKey -- focus key currently being timed local settledTime -- how long it has been unchanged local spokenKey -- focus key we have already spoken details for local lastState -- so leaving a screen and coming back speaks again -- Pure, and exposed for the testbed: advance the timer for `key` by `dt` and say -- whether details are due now. Due exactly once per focus key -- without that, -- a stationary cursor would re-speak every frame. function details.settleStep(key, dt) if key ~= settledKey then settledKey = key settledTime = 0 return false end if key == nil or key == spokenKey then return false end settledTime = (settledTime or 0) + (dt or 0) if settledTime < details.config.settleDelay then return false end spokenKey = key return true end function details.reset() settledKey = nil settledTime = nil spokenKey = nil end -- Called once per frame from init.lua's update hook. function details.update(dt) if not details.config.autoSpeak then return end local stateName = currentState() if not stateName then return end -- The focus key is state-qualified, so without this, leaving a screen and -- returning to the same slot would be "already spoken" and stay silent. if stateName ~= lastState then lastState = stateName details.reset() end local descriptor = menus.loadDescriptor(stateName) if not descriptor or not descriptor.details then details.reset() return end -- Only the focus KEY is wanted here; readFocus's text is what read() already -- announced and must not be repeated. local _, key = menus.readFocus(stateName) -- No focus key means the descriptor could not read the screen this frame -- (mid-transition, window not built yet). Don't time an unknown. if key == nil then details.reset() return end if not details.settleStep(stateName .. "|" .. key, dt) then return end local blocks = details.get(stateName, false) if not blocks then return end -- Queued, not interrupting: this is background awareness the player did not -- ask for, so anything they DID ask for outranks it. speakBlocks(blocks, false) end function details.registerKeys(hotkeys) hotkeys.bind("=", nil, function() details.speak(false) end) hotkeys.bind("=", "shift", function() details.speak(true) end) end return details