Files
alchemist-accessibility/accessibility/patches/hotbar.lua
T

251 lines
8.1 KiB
Lua
Raw Normal View History

-- Patch: views/hotbar.lua -- the hotbar keys 1-0.
--
-- Pressing a hotbar key runs `hotkey.action(state)` (models/hotkey.lua). For an
-- equipment binding that resolves to one of the entries in
-- modules/item/itemActions.lua and calls its onActivation directly -- Wield,
-- Drink, Switch on, Pack away and so on. None of those print to the console;
-- most only play a sound. So by ear a hotbar press was a click and nothing else:
-- you could not tell what item it was, what it did to it, or whether the key had
-- even registered.
--
-- We wrap the hotbar VIEW factory rather than models/hotkey, because
-- models/hotbar.lua captures `newHotkey` as a file-local at require time -- long
-- before we run -- so replacing package.loaded["models.hotkey"] would change
-- nothing. views/hotbar is only ever required by states/{ingame,inventory,
-- actions_menu}, which load lazily.
--
-- Three things are narrated:
--
-- * activating a slot -- "Torch, Switch on" / "Action: Throw"
-- * an empty slot -- silent in game; indistinguishable from a dropped key
-- * binding / unbinding -- self.bind plays a sound and says nothing
--
-- ORDERING: like patches/slider.lua this replaces package.loaded, so it only
-- affects requires that happen afterwards. Safe for the same reason: states are
-- loaded lazily by states.lua, and applyPatches() runs before the self-test.
local speech = require("accessibility.speech")
--------------------------------------------------------------------------------
-- Which itemAction a hotbar equipment command resolves to.
--
-- This mirrors `itemActionsByEquipmentCommand` + `findItemAction` in
-- models/hotkey.lua, which are file-locals with no getter. We need it BEFORE the
-- action runs: afterwards the item's `isEquipped` has flipped and the conditions
-- would pick the opposite verb ("Unwield" for a press that wielded).
--
-- hotbar.actionIdsFor is exported so the testbed can assert every id still
-- exists and still has a header -- the version-bump canary for this table.
--------------------------------------------------------------------------------
local hotbar = {}
hotbar.actionIdsFor = {
equipUnequip = {7, 8, 9, 10, 13, 14},
prepareThrow = {2, 3},
drink = {4},
apply = {5},
eat = {6},
read = {11},
open = {12},
activate = {1},
}
function hotbar.headerFor(item, equipmentCommand)
local ids = hotbar.actionIdsFor[equipmentCommand]
if not ids then
return nil
end
local itemActions = require("modules.item.itemActions")
local player = require("models.player")
for _, id in ipairs(ids) do
local action = itemActions[id]
if action and (not action.condition or action.condition(item, player)) then
return action.header
end
end
return nil
end
-- The current action, phrased as accessibility/status.lua phrases it.
local function currentActionLabel()
local player = require("models.player")
local action = player.currentSelectedAction
if not action then
return nil
end
-- `title` is the display string; `name` is the internal id ("interact"), and
-- setCurrentAction does not copy `title`, so the fallback is the usual path.
local label = action.title
if not label then
label = action.name or "unknown"
label = label:sub(1, 1):upper() .. label:sub(2)
end
return label
end
--------------------------------------------------------------------------------
-- What to say for one slot, decided BEFORE the action runs.
-- Returns a string, or nil to stay quiet, plus a flag asking for the current
-- action to be read afterwards instead (the binding changed it).
--------------------------------------------------------------------------------
local function describeActivation(packed)
if packed.type == "action" then
return nil, true
end
if packed.type ~= "equipment" then
return nil, false
end
local player = require("models.player")
local item = player.inventory and player.inventory.findItemByTemplateId(packed.itemTemplateId)
if not item then
-- The game itself prints "<name> not found in your inventory." here, and
-- patches/console.lua speaks it. Saying anything more would double up.
return nil, false
end
local name = item.name
if not name then
local templates = require("modules.item.templates")
local template = templates[packed.itemTemplateId]
name = template and template.name or "Item"
end
local header = hotbar.headerFor(item, packed.command)
if not header then
-- getAction bails out silently in this case. The one exception is
-- prepareThrow with autoSwitchToThrow, which still switches the action.
if packed.command == "prepareThrow" then
local ok, config = pcall(require, "models.config")
if ok and config.get("autoSwitchToThrow") then
return nil, true
end
end
return name .. ", no action available", false
end
return name .. ", " .. header, false
end
--------------------------------------------------------------------------------
function hotbar.apply()
local newHotbarView = require("views.hotbar")
if type(newHotbarView) ~= "function" then
error("views.hotbar is not a factory function -- game structure changed")
end
package.loaded["views.hotbar"] = function(params)
local view = newHotbarView(params)
if type(view) ~= "table" then
return view
end
-- refreshModel swaps the model when the player switches hotbar page, so
-- track it rather than holding params.hotbarModel forever.
local model = params and params.hotbarModel
local realRefresh = view.refreshModel
if type(realRefresh) == "function" then
view.refreshModel = function(newModel, ...)
model = newModel
return realRefresh(newModel, ...)
end
end
local realCheckKeypress = view.checkKeypress
if type(realCheckKeypress) == "function" then
view.checkKeypress = function(state, scancode, bindCallback)
local text, readAction
-- Only the activate hotbars act on a press; on the bind hotbars
-- (inventory / actions menu) the same key opens a bind menu,
-- which those states' descriptors already narrate.
if params and params.actionOnClick == "activate" then
pcall(function()
local keybindings = require("models.keybindings")
local command = keybindings.getCommand(scancode, "hotbar")
local index = command and tonumber(command)
if not index then
return
end
local hotkey = model and model.hotkeys and model.hotkeys[index]
if not hotkey then
text = "Hotbar " .. index .. ", empty"
return
end
-- pack() is the only public view of what a hotkey holds:
-- {type, command, index, itemTemplateId}.
local packed = hotkey.pack()
text, readAction = describeActivation(packed)
end)
end
local result = realCheckKeypress(state, scancode, bindCallback)
pcall(function()
if readAction then
local label = currentActionLabel()
text = label and ("Action: " .. label) or text
end
if text then
-- Interrupting: this is the answer to the key just pressed.
speech.say(text, true)
end
end)
return result
end
end
-- Binding and unbinding are sound-only in game (sound.bind / sound.unbind).
local realBind = view.bind
if type(realBind) == "function" then
view.bind = function(bindParams, index)
-- self.bind mutates bindParams (it sets .index) and reset()s the
-- selection, so read what we need first.
local slot = index or (view.isReadyForBinding and view.isReadyForBinding())
local existing = slot and model and model.hotkeys and model.hotkeys[slot]
local result = realBind(bindParams, index)
pcall(function()
if not slot then
return
end
local nowBound = model and model.hotkeys and model.hotkeys[slot]
if nowBound then
local what
if bindParams.type == "equipment" then
local templates = require("modules.item.templates")
local template = templates[bindParams.itemTemplateId]
what = template and template.name
else
what = bindParams.command
end
speech.say((what or "Bound") .. " bound to hotbar " .. slot, true)
elseif existing then
speech.say("Hotbar " .. slot .. " cleared", true)
end
end)
return result
end
end
return view
end
end
return hotbar