Files
alchemist-accessibility/accessibility/states/_itemgrid.lua
T
Accessibility ModandClaude Opus 5 0df3842bf1 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>
2026-08-02 02:59:21 +02:00

155 lines
5.1 KiB
Lua

-- Shared descriptor for the item-grid screens: inventory, craft, refine, trade.
--
-- All four build a `views/inventoryWindow` and move a cursor over a grid of
-- cells. The inventoryCursorPopup patch speaks item details, but ONLY when a cell
-- actually holds an item -- so empty cells and empty tabs were silent and you
-- could not tell the cursor had moved. This owns narration for these screens
-- instead (patches/popups.lua stands down wherever a descriptor exists).
--
-- 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")
return function(options)
options = options or {}
local windowName = options.window or "inventoryWindow"
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 {
reset = function()
lastTab = nil
lastMenu = nil
end,
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
-- speech (see patches/slider.lua). Announcing the grid cell over the
-- top of it would fight the number the player is adjusting.
local confirming = menus.getAnyUpvalue(stateTable, "confirmationWindowEnabled")
if confirming then
return nil, nil
end
local position = window.cursorPosition
if type(position) ~= "number" then
return nil, nil
end
-- activeTabIndex lives inside the window closure, not the state file,
-- so reach it through one of the window's own methods.
local tabIndex
if window.adjustTab then
tabIndex = menus.getUpvalue(window.adjustTab, "activeTabIndex")
end
local parts = {}
if tabIndex and tabIndex ~= lastTab then
lastTab = tabIndex
local category = itemCategories[tabIndex]
parts[#parts + 1] = (category and category.header) or ("tab " .. tabIndex)
end
local ok, item = pcall(window.getItemOnCursor)
if ok and item then
parts[#parts + 1] = item.name or item.templateId or "item"
if item.quantity and item.quantity > 1 then
parts[#parts + 1] = "x" .. item.quantity
end
if item.isEquipped then
parts[#parts + 1] = "equipped"
end
if item.isUnavailable then
parts[#parts + 1] = "unavailable"
end
else
parts[#parts + 1] = "empty"
end
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, ", "),
table.concat({
tostring(tabIndex), tostring(position),
tostring(ok and item and (item.name or "?")),
tostring(ok and item and item.isEquipped or false),
}, ":")
end,
}
end