hotbar: say what the key you pressed just did
Pressing a hotbar key ran hotkey.action(state), which for an equipment binding calls an itemActions entry's onActivation directly -- Wield, Drink, Switch on, Pack away. None of those reach console.print or a popup; most only play a sound. So the whole hotbar was silent: no item name, no verb, no confirmation the key had registered at all. patches/hotbar.lua wraps the hotbar VIEW factory, not models/hotkey: models/hotbar.lua captures newHotkey as a file-local at require time, and models/world.lua captures newHotbar the same way, both long before our entry point. views/hotbar.lua is required only by the three states, which load lazily -- the same ordering argument as patches/slider.lua. The verb is computed BEFORE the action runs. The itemAction conditions key off item.isEquipped, which the action itself flips, so asked afterwards they return the opposite verb and a press that wielded would announce "Unwield". Also narrates the empty slot, the silent no-applicable-action branch, binding and unbinding (sound-only in game), and action-type slots. Says nothing when the item is missing from the inventory -- the game prints that itself and patches/console.lua speaks it. 78 testbed checks, 0 failures. The prologue gives the player no items, so the live path is exercised by planting a torch, binding it to slot 1, pressing 1 twice and putting the world back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1f48dea550
commit
55b19dd537
@@ -0,0 +1,250 @@
|
||||
-- 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
|
||||
Reference in New Issue
Block a user