208 lines
6.4 KiB
Lua
208 lines
6.4 KiB
Lua
-- 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
|