inventory: speak the item action menu
Pressing enter/z on an inventory item plays a click and, until now, said nothing. The click is sound.select: the menu WAS opening every time. inventory.keypressed -> inventoryWindow.enableItemMenu() builds a second, nested selection over the grid (modules/item/itemMenu.lua) listing the actions that item currently allows, filtered from itemActions.lua by per-action conditions: Activate, Wield/Unwield, Wear/Take off, Switch on/off, Drink, Eat, Apply, Read, Open, Prepare for throwing/Pack away. Up/down move in it, enter activates, any other key cancels. None of it goes through console.print or the cursor popup, so it was silent -- and since the game has no separate equipment screen, this menu is the only way to equip anything. Reported by Talon: the torch quest was unplayable. _itemgrid.lua now checks window.itemMenu first and keeps the grid cell quiet underneath, since the menu is modal over it. The menu is a closure with no getters, so lines and currentSelectedAction are read as upvalues of its own draw(). The item name leads only when the menu opens, and the "just opened" memory is the menu table itself rather than a flag, which keeps read() idempotent -- a flag would be consumed by the focus poll and the real announcement would come out headless. Items also now read "equipped", which is part of the focus key on purpose: after Switch on the menu closes with the cursor unmoved, so otherwise the re-read is suppressed as unchanged and the action still feels silent. Now: "Switch on" -> "a torch, equipped, slot 3". New patch (the 6th) wraps the inventoryWindow factory for the dead case: when getActionsList returns nil -- an item with no applicable actions -- the game does literally nothing, not even the click, which by ear is indistinguishable from a dropped keypress. Says "<item>, no actions". The prologue inventory is empty, so the live path is unverified; the testbed instead builds an itemMenu from a synthetic item and asserts the two upvalue names still resolve. That is the version-bump canary for this descriptor, not proof of the wording. 38 checks, 0 failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7ce429c2fb
commit
0df3842bf1
@@ -144,6 +144,35 @@ than one. The page text also goes into the message history, so `[` and `]` will
|
|||||||
walk back through it and `\` re-reads a line — useful for a long lore entry you
|
walk back through it and `\` re-reads a line — useful for a long lore entry you
|
||||||
want to take in a piece at a time.
|
want to take in a piece at a time.
|
||||||
|
|
||||||
|
### The inventory (`i`)
|
||||||
|
|
||||||
|
Moving the cursor reads the item under it, or `empty`, plus the slot number:
|
||||||
|
`a torch, slot 3`. `q` / `e` change tab, and the tab name is spoken when it
|
||||||
|
changes. An item you are currently wearing, wielding or have switched on is read
|
||||||
|
as `equipped`.
|
||||||
|
|
||||||
|
Pressing enter (or `z`) on an item opens a small menu of the things you can do
|
||||||
|
with *that* item right now — the list is different for every item, and changes
|
||||||
|
with its state. It is read as, for example:
|
||||||
|
|
||||||
|
```
|
||||||
|
a torch, actions, Switch on, 1 of 2
|
||||||
|
```
|
||||||
|
|
||||||
|
Up and Down move through the actions, enter performs the selected one, and any
|
||||||
|
other key closes the menu without doing anything. After the action runs you hear
|
||||||
|
the item again, so `Switch on` is followed by `a torch, equipped, slot 3` — that
|
||||||
|
is your confirmation it worked.
|
||||||
|
|
||||||
|
Some items have no actions at all (plain crafting reagents, for instance). For
|
||||||
|
those, enter does nothing whatsoever in the base game — not even a click — so the
|
||||||
|
mod says `a pinch of sulphur, no actions` rather than leaving you wondering
|
||||||
|
whether the key registered.
|
||||||
|
|
||||||
|
Common actions include Activate, Wield, Unwield, Wear, Take off, Switch on,
|
||||||
|
Switch off, Drink, Eat, Apply, Read, Open, and Prepare for throwing / Pack away.
|
||||||
|
**Equipping is done from here** — there is no separate equipment screen.
|
||||||
|
|
||||||
### Your character
|
### Your character
|
||||||
|
|
||||||
| Key | Action |
|
| Key | Action |
|
||||||
|
|||||||
@@ -239,6 +239,7 @@ function accessibility.initialize()
|
|||||||
accessibility.registerPatch("popups", require("accessibility.patches.popups"))
|
accessibility.registerPatch("popups", require("accessibility.patches.popups"))
|
||||||
accessibility.registerPatch("slider", require("accessibility.patches.slider"))
|
accessibility.registerPatch("slider", require("accessibility.patches.slider"))
|
||||||
accessibility.registerPatch("popupstack", require("accessibility.patches.popupstack"))
|
accessibility.registerPatch("popupstack", require("accessibility.patches.popupstack"))
|
||||||
|
accessibility.registerPatch("itemmenu", require("accessibility.patches.itemmenu"))
|
||||||
|
|
||||||
pcall(function()
|
pcall(function()
|
||||||
require("accessibility.surroundings").registerKeys(hotkeys)
|
require("accessibility.surroundings").registerKeys(hotkeys)
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
-- Patch: views/inventoryWindow.lua -- the "this item has no actions" case.
|
||||||
|
--
|
||||||
|
-- Pressing enter/z on an inventory cell calls window.enableItemMenu(), which
|
||||||
|
-- builds the item's action list via modules/item/getActionsList.lua. That list is
|
||||||
|
-- filtered by per-action `condition`s, and getActionsList returns NIL when none
|
||||||
|
-- of them apply -- a plain reagent, say, which you can neither wield, drink, read
|
||||||
|
-- nor switch on. newItemMenu then returns nil, enableItemMenu returns nil, and
|
||||||
|
-- the game does *literally nothing*: no menu, and not even the sound.select click
|
||||||
|
-- it plays on success.
|
||||||
|
--
|
||||||
|
-- Sighted players see the absence of a popup. By ear that is indistinguishable
|
||||||
|
-- from a dropped keypress, so this says so out loud. The successful case is NOT
|
||||||
|
-- announced here -- accessibility/states/_itemgrid.lua reads the open menu.
|
||||||
|
--
|
||||||
|
-- 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")
|
||||||
|
|
||||||
|
return function()
|
||||||
|
local newInventoryWindow = require("views.inventoryWindow")
|
||||||
|
|
||||||
|
if type(newInventoryWindow) ~= "function" then
|
||||||
|
error("views.inventoryWindow is not a factory function -- game structure changed")
|
||||||
|
end
|
||||||
|
|
||||||
|
package.loaded["views.inventoryWindow"] = function(...)
|
||||||
|
local window = newInventoryWindow(...)
|
||||||
|
|
||||||
|
local realEnable = window.enableItemMenu
|
||||||
|
if type(realEnable) ~= "function" then
|
||||||
|
return window
|
||||||
|
end
|
||||||
|
|
||||||
|
window.enableItemMenu = function(...)
|
||||||
|
local result = realEnable(...)
|
||||||
|
|
||||||
|
if not result then
|
||||||
|
pcall(function()
|
||||||
|
local ok, item = pcall(window.getItemOnCursor)
|
||||||
|
if not ok or not item then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
speech.say((item.name or "This item") .. ", no actions", true)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
return window
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -7,6 +7,18 @@
|
|||||||
-- instead (patches/popups.lua stands down wherever a descriptor exists).
|
-- instead (patches/popups.lua stands down wherever a descriptor exists).
|
||||||
--
|
--
|
||||||
-- Returns a descriptor; call with the state's window upvalue name.
|
-- Returns a descriptor; call with the state's window upvalue name.
|
||||||
|
--
|
||||||
|
-- Second thing this owns: the ITEM ACTION MENU. Pressing enter/z on an occupied
|
||||||
|
-- cell calls inventoryWindow.enableItemMenu(), which builds a little popup list
|
||||||
|
-- of the actions that item currently allows ("Switch on", "Wield", "Drink",
|
||||||
|
-- "Read"...) from modules/item/getActionsList.lua. It is a second, nested
|
||||||
|
-- selection -- up/down move within it, enter activates, anything else cancels --
|
||||||
|
-- and nothing about it goes through console.print or the cursor popup, so it was
|
||||||
|
-- completely silent. All the player heard was the sound.select click that
|
||||||
|
-- enableItemMenu plays on success.
|
||||||
|
--
|
||||||
|
-- The menu is a closure (modules/item/itemMenu.lua) with no getters, so its
|
||||||
|
-- `lines` and `currentSelectedAction` are read as upvalues of its own draw().
|
||||||
|
|
||||||
local itemCategories = require("modules.item.categories")
|
local itemCategories = require("modules.item.categories")
|
||||||
|
|
||||||
@@ -15,9 +27,71 @@ return function(options)
|
|||||||
|
|
||||||
local windowName = options.window or "inventoryWindow"
|
local windowName = options.window or "inventoryWindow"
|
||||||
local lastTab
|
local lastTab
|
||||||
|
local lastMenu
|
||||||
|
|
||||||
|
-- Read the open item action menu, if there is one.
|
||||||
|
local function readItemMenu(window, menus)
|
||||||
|
local itemMenu = window.itemMenu
|
||||||
|
if type(itemMenu) ~= "table" or type(itemMenu.draw) ~= "function" then
|
||||||
|
lastMenu = nil
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local lines = menus.getUpvalue(itemMenu.draw, "lines")
|
||||||
|
local selected = menus.getUpvalue(itemMenu.draw, "currentSelectedAction")
|
||||||
|
|
||||||
|
if type(lines) ~= "table" or type(selected) ~= "number" then
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local line = lines[selected]
|
||||||
|
if type(line) ~= "table" or not line.text then
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local parts = {}
|
||||||
|
|
||||||
|
-- Lead with the item only when this menu just opened, so arrowing through
|
||||||
|
-- two or three actions doesn't repeat the name every time. `lastMenu` holds
|
||||||
|
-- the menu table itself rather than a flag, which keeps read() idempotent:
|
||||||
|
-- calling it twice for the same focus produces the same words both times.
|
||||||
|
if itemMenu ~= lastMenu then
|
||||||
|
lastMenu = itemMenu
|
||||||
|
local ok, item = pcall(window.getItemOnCursor)
|
||||||
|
if ok and item and item.name then
|
||||||
|
parts[#parts + 1] = item.name
|
||||||
|
end
|
||||||
|
parts[#parts + 1] = "actions"
|
||||||
|
end
|
||||||
|
|
||||||
|
parts[#parts + 1] = line.text
|
||||||
|
parts[#parts + 1] = selected .. " of " .. #lines
|
||||||
|
|
||||||
|
return table.concat(parts, ", "),
|
||||||
|
"itemmenu:" .. tostring(selected) .. "/" .. tostring(#lines)
|
||||||
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
reset = function()
|
||||||
|
lastTab = nil
|
||||||
|
lastMenu = nil
|
||||||
|
end,
|
||||||
|
|
||||||
read = function(stateTable, menus)
|
read = function(stateTable, menus)
|
||||||
|
local window = menus.getAnyUpvalue(stateTable, windowName)
|
||||||
|
|
||||||
|
if type(window) ~= "table" or not window.getItemOnCursor then
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The action menu is modal over the grid: while it is open, up/down
|
||||||
|
-- move within IT, not over the cells. So it is checked first, and the
|
||||||
|
-- grid cell stays quiet underneath it.
|
||||||
|
if window.itemMenu then
|
||||||
|
return readItemMenu(window, menus)
|
||||||
|
end
|
||||||
|
lastMenu = nil
|
||||||
|
|
||||||
-- While the quantity confirmation window is open, the slider owns
|
-- While the quantity confirmation window is open, the slider owns
|
||||||
-- speech (see patches/slider.lua). Announcing the grid cell over the
|
-- speech (see patches/slider.lua). Announcing the grid cell over the
|
||||||
-- top of it would fight the number the player is adjusting.
|
-- top of it would fight the number the player is adjusting.
|
||||||
@@ -26,12 +100,6 @@ return function(options)
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local window = menus.getAnyUpvalue(stateTable, windowName)
|
|
||||||
|
|
||||||
if type(window) ~= "table" or not window.getItemOnCursor then
|
|
||||||
return nil, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local position = window.cursorPosition
|
local position = window.cursorPosition
|
||||||
if type(position) ~= "number" then
|
if type(position) ~= "number" then
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -59,6 +127,9 @@ return function(options)
|
|||||||
if item.quantity and item.quantity > 1 then
|
if item.quantity and item.quantity > 1 then
|
||||||
parts[#parts + 1] = "x" .. item.quantity
|
parts[#parts + 1] = "x" .. item.quantity
|
||||||
end
|
end
|
||||||
|
if item.isEquipped then
|
||||||
|
parts[#parts + 1] = "equipped"
|
||||||
|
end
|
||||||
if item.isUnavailable then
|
if item.isUnavailable then
|
||||||
parts[#parts + 1] = "unavailable"
|
parts[#parts + 1] = "unavailable"
|
||||||
end
|
end
|
||||||
@@ -68,10 +139,15 @@ return function(options)
|
|||||||
|
|
||||||
parts[#parts + 1] = "slot " .. position
|
parts[#parts + 1] = "slot " .. position
|
||||||
|
|
||||||
|
-- isEquipped is part of the focus key on purpose: after "Switch on"
|
||||||
|
-- the menu closes and the cursor has not moved, so without it the
|
||||||
|
-- re-read would be suppressed as unchanged and the player would get no
|
||||||
|
-- confirmation that the action took effect.
|
||||||
return table.concat(parts, ", "),
|
return table.concat(parts, ", "),
|
||||||
table.concat({
|
table.concat({
|
||||||
tostring(tabIndex), tostring(position),
|
tostring(tabIndex), tostring(position),
|
||||||
tostring(ok and item and (item.name or "?")),
|
tostring(ok and item and (item.name or "?")),
|
||||||
|
tostring(ok and item and item.isEquipped or false),
|
||||||
}, ":")
|
}, ":")
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -435,6 +435,35 @@ local function scenario()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- The item action menu (enter/z on an inventory item) is a closure with no
|
||||||
|
-- getters, so _itemgrid.lua reads its `lines` and `currentSelectedAction`
|
||||||
|
-- as upvalues of its draw(). The prologue inventory is empty, so the menu
|
||||||
|
-- cannot be opened for real here -- but the fragile part is the upvalue
|
||||||
|
-- NAMES, and those can be checked by building a menu directly. This is the
|
||||||
|
-- version-bump canary for that descriptor.
|
||||||
|
do
|
||||||
|
local newItemMenu = require("modules.item.itemMenu")
|
||||||
|
local fakeItem = {name = "a test item", onUse = function() end}
|
||||||
|
local itemMenu = newItemMenu(fakeItem, 0, 0)
|
||||||
|
|
||||||
|
check("item action menu builds for a usable item", itemMenu ~= nil)
|
||||||
|
|
||||||
|
if itemMenu then
|
||||||
|
local lines = menus.getUpvalue(itemMenu.draw, "lines")
|
||||||
|
local selected = menus.getUpvalue(itemMenu.draw, "currentSelectedAction")
|
||||||
|
|
||||||
|
check("item action menu exposes its action list by upvalue",
|
||||||
|
type(lines) == "table" and lines[1] and lines[1].text ~= nil,
|
||||||
|
"lines=" .. tostring(lines))
|
||||||
|
check("item action menu exposes its selection by upvalue",
|
||||||
|
selected == 1, "currentSelectedAction=" .. tostring(selected))
|
||||||
|
|
||||||
|
itemMenu.adjustSelectedAction(1)
|
||||||
|
check("item action menu selection is readable after it moves",
|
||||||
|
menus.getUpvalue(itemMenu.draw, "currentSelectedAction") ~= nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- The journal's whole point is the page of text on the right-hand side; the
|
-- The journal's whole point is the page of text on the right-hand side; the
|
||||||
-- list of titles on the left is just an index into it. Announcing only the
|
-- list of titles on the left is just an index into it. Announcing only the
|
||||||
-- title (which is what it used to do) reads as "there is an entry here"
|
-- title (which is what it used to do) reads as "there is an entry here"
|
||||||
|
|||||||
Reference in New Issue
Block a user