The item grids said which item you were on ("a torch, slot 3") but nothing
about what it was -- no description, no condition, and no "what does this
refine into", which is the whole of alchemy.
The game already composes all of it in one place: item.getPopupMessage()
(modules/item.lua:199) returns the cursor popup's line list, so read that
rather than re-deriving anything from templates.
It also already gates it behind a dwell -- the popup only fades in after
examinationPopupFadeInDuration (0.4s), so a sighted player who arrows past a
slot never sees the details either. Mirror that instead of inventing a
modality: stop on an item for 0.6s and the details speak themselves, queued.
"=" reads them on demand, Shift+= keeps the name line too. ("r" was the
obvious mnemonic but is a game-claimed scancode, and getCommand() dispatches
on scancode ignoring modifiers.)
New generic hook descriptor.details() returning a LIST of blocks, so each is
also pushed into the console history and [ ] \ walk a long description back
afterwards -- the journal page trick. Only _itemgrid.lua implements it so far.
The other half is the hint panel craft/refine/trade draw permanently beside
the grid, which the popup never contains: recipe components, and price, which
appears nowhere else in the game. The three panels share a shape, so one
reader covers them. Affordability survives only as a red colour in the hint
table and decides whether enter does anything, so it is decoded and spoken.
59 testbed checks, 0 failures: a synthetic ivy_leaves item as the version-bump
canary for getPopupMessage, hasUpvalue over every hint-panel local, four
assertions on the pure settleStep, and an end-to-end "=" press. Not yet
exercised against a live item -- the prologue inventory is empty.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
146 lines
4.8 KiB
Lua
146 lines
4.8 KiB
Lua
-- hotkeys.lua — the mod's global key layer.
|
|
--
|
|
-- Sits in front of states.keypressed. Keys we own are handled and CONSUMED (the
|
|
-- game never sees them); everything else falls straight through.
|
|
--
|
|
-- Consuming matters more than it looks. keybindings.getCommand() dispatches on
|
|
-- scancode alone and ignores modifiers, checking held modifiers separately via
|
|
-- isCommandDown. So Ctrl+Up is seen by the game as plain "up" — a movement — with
|
|
-- moveCamera held. If we bound anything to Ctrl+Up and passed it through, the
|
|
-- player would move. Hence: our keys are chosen from scancodes the game does not
|
|
-- bind at all, and we still consume them.
|
|
--
|
|
-- Keys the game claims by default (do not use):
|
|
-- . 0-9 ` a b backspace c d delete down e escape f g i j kp1-kp9 kpenter
|
|
-- lalt lctrl left lshift mouse3 q r ralt rctrl return right rshift s space
|
|
-- t tab up x y z
|
|
--
|
|
-- Note Ctrl = moveCamera and Shift = repeatAction/adjustByFive are HELD modifiers
|
|
-- in game. Combining them with an unbound scancode (e.g. Ctrl+PageUp) is safe,
|
|
-- because the game maps the scancode to no command and does nothing.
|
|
|
|
local speech = require("accessibility.speech")
|
|
local history = require("accessibility.history")
|
|
|
|
local hotkeys = {}
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Where our keys must stay out of the way
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- States that accept free text. Swallowing "[" while the player is naming a
|
|
-- character or typing in the developer console would be a real bug.
|
|
local textEntryStates = {
|
|
ingame_say = true,
|
|
new_game = true,
|
|
keybindings_menu = true, -- captures raw keys for rebinding
|
|
}
|
|
|
|
local function isTextEntryState()
|
|
local ok, states = pcall(require, "states")
|
|
if not ok then
|
|
return false
|
|
end
|
|
return textEntryStates[states.getCurrent()] == true
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Modifier helpers
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- Indirected through overridable fields so the testbed can drive modified
|
|
-- hotkeys. love.keyboard.isScancodeDown reads real hardware state, which a
|
|
-- synthetic love.event.push cannot influence -- without this seam, every
|
|
-- modifier-gated key would be untestable.
|
|
hotkeys.isCtrlDown = function()
|
|
return love.keyboard.isScancodeDown("lctrl", "rctrl")
|
|
end
|
|
|
|
hotkeys.isShiftDown = function()
|
|
return love.keyboard.isScancodeDown("lshift", "rshift")
|
|
end
|
|
|
|
local function ctrlDown()
|
|
return hotkeys.isCtrlDown()
|
|
end
|
|
|
|
local function shiftDown()
|
|
return hotkeys.isShiftDown()
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Bindings
|
|
--
|
|
-- Each entry: scancode -> { {mods, action}, ... }, checked in order.
|
|
-- mods: "ctrl", "shift", or nil for "no modifier required".
|
|
--------------------------------------------------------------------------------
|
|
|
|
local bindings = {}
|
|
|
|
local function bind(scancode, mods, action)
|
|
bindings[scancode] = bindings[scancode] or {}
|
|
table.insert(bindings[scancode], {mods = mods, action = action})
|
|
end
|
|
|
|
-- Console review
|
|
bind("[", nil, history.reviewPrevious)
|
|
bind("]", nil, history.reviewNext)
|
|
bind("[", "shift", history.reviewFirst)
|
|
bind("]", "shift", history.reviewLast)
|
|
bind("\\", nil, history.repeatCurrent)
|
|
bind("\\", "shift", history.copyCurrent)
|
|
|
|
-- Other keys are registered by their own modules via hotkeys.bind, from
|
|
-- init.lua: surroundings, tracker, status, viewwatch, details.
|
|
|
|
function hotkeys.bind(scancode, mods, action)
|
|
bind(scancode, mods, action)
|
|
end
|
|
|
|
local function matches(entry)
|
|
if entry.mods == "ctrl" then
|
|
return ctrlDown()
|
|
elseif entry.mods == "shift" then
|
|
return shiftDown()
|
|
end
|
|
-- No modifier required: reject if one is held, so Shift+[ doesn't also fire [.
|
|
return not ctrlDown() and not shiftDown()
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Dispatch
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- Returns true if the key was ours and should NOT reach the game.
|
|
function hotkeys.keypressed(_key, scancode)
|
|
if isTextEntryState() then
|
|
return false
|
|
end
|
|
|
|
local candidates = bindings[scancode]
|
|
if not candidates then
|
|
return false
|
|
end
|
|
|
|
for _, entry in ipairs(candidates) do
|
|
if matches(entry) then
|
|
local ok, err = pcall(entry.action)
|
|
if not ok then
|
|
print("[accessibility] hotkey '" .. tostring(scancode) .. "' error: " .. tostring(err))
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-- Any key that reaches the game is an explicit player action, so it cancels the
|
|
-- pending speech backlog. Whatever that action produces is then spoken fresh.
|
|
-- This is the "speak everything queued, unless explicitly stopped" policy.
|
|
function hotkeys.onGameKey()
|
|
speech.silence()
|
|
end
|
|
|
|
return hotkeys
|