-- 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 " 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