Game-file footprint unchanged: still one line in main.lua. - describe.lua: shared spatial formatting. 8-point compass and Chebyshev distance, since the game moves 8-directionally and Euclidean would overstate diagonal travel. Verbosity setting, because sweeping eight neighbours and inspecting a single tile want different amounts of detail. - states/ingame_observe.lua: narrates the game's existing examine cursor. - surroundings.lua: probe one direction, scan all eight, or report location, without moving anything or touching game state. - patches/popups.lua: wraps inventoryCursorPopup.set, covering inventory, craft, refine and trade in a single patch. Q3 resolved as augment rather than relay: the game's descriptions carry no position, which is fine on screen and useless by ear, so distance and direction are appended. Examine mode is polled rather than driven by levelCursorPopup. That popup is set from an animation-completion callback, so unless the player has enabled the game's quickExaminationPopup option the text arrives a full keypress late -- in testing you heard the previous tile while standing on the next. Polling level.cursorX/Y is same-frame and setting-independent. hotkeys.isCtrlDown/isShiftDown are now overridable fields rather than direct love.keyboard calls: real hardware state cannot be faked by love.event.push, so without that seam every modifier-gated hotkey would be untestable. 11/11 testbed checks, history unit tests passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
77 lines
2.1 KiB
Lua
77 lines
2.1 KiB
Lua
-- Patch: views/levelCursorPopup.lua and views/inventoryCursorPopup.lua
|
|
--
|
|
-- `inventoryCursorPopup.set` is the convergence point for focus text in four
|
|
-- screens: inventory, craft, refine and trade. Patching it narrates all four
|
|
-- without touching any state file.
|
|
--
|
|
-- inventoryCursorPopup.set(lines, x, y, alignX, alignY) -- lines is a TABLE
|
|
-- of {text=, icon=}
|
|
--
|
|
-- NOTE: levelCursorPopup (the examine cursor) is deliberately NOT handled here.
|
|
-- It is set from an animation-completion callback, so its text lands a keypress
|
|
-- late unless the player enables the game's "quickExaminationPopup" option.
|
|
-- accessibility/states/ingame_observe.lua polls the cursor position instead,
|
|
-- which is immediate and setting-independent.
|
|
|
|
local speech = require("accessibility.speech")
|
|
|
|
-- Suppress repeats: the setter is called on every mouse move, often with
|
|
-- unchanged content.
|
|
local lastInventoryText
|
|
|
|
local function linesToText(lines)
|
|
if type(lines) ~= "table" then
|
|
return nil
|
|
end
|
|
|
|
local parts = {}
|
|
for _, line in ipairs(lines) do
|
|
if type(line) == "table" and line.text then
|
|
parts[#parts + 1] = line.text
|
|
elseif type(line) == "string" then
|
|
parts[#parts + 1] = line
|
|
end
|
|
end
|
|
|
|
if #parts == 0 then
|
|
return nil
|
|
end
|
|
|
|
return table.concat(parts, ". ")
|
|
end
|
|
|
|
return function()
|
|
----------------------------------------------------------------------------
|
|
-- inventoryCursorPopup: inventory, craft, refine, trade
|
|
----------------------------------------------------------------------------
|
|
local inventoryPopup = require("views.inventoryCursorPopup")
|
|
|
|
local realInventorySet = inventoryPopup.set
|
|
if not realInventorySet then
|
|
error("views.inventoryCursorPopup has no set function -- game structure changed")
|
|
end
|
|
|
|
inventoryPopup.set = function(lines, ...)
|
|
local result = realInventorySet(lines, ...)
|
|
|
|
pcall(function()
|
|
local text = linesToText(lines)
|
|
|
|
if not text then
|
|
lastInventoryText = nil
|
|
return
|
|
end
|
|
|
|
text = speech.normalise(text)
|
|
if not text or text == lastInventoryText then
|
|
return
|
|
end
|
|
lastInventoryText = text
|
|
|
|
speech.say(text, true)
|
|
end)
|
|
|
|
return result
|
|
end
|
|
end
|