diff --git a/accessibility/README.md b/accessibility/README.md index a6c39c1..c84ff85 100644 --- a/accessibility/README.md +++ b/accessibility/README.md @@ -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 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 | Key | Action | diff --git a/accessibility/init.lua b/accessibility/init.lua index dc3bf80..879e03b 100644 --- a/accessibility/init.lua +++ b/accessibility/init.lua @@ -239,6 +239,7 @@ function accessibility.initialize() accessibility.registerPatch("popups", require("accessibility.patches.popups")) accessibility.registerPatch("slider", require("accessibility.patches.slider")) accessibility.registerPatch("popupstack", require("accessibility.patches.popupstack")) + accessibility.registerPatch("itemmenu", require("accessibility.patches.itemmenu")) pcall(function() require("accessibility.surroundings").registerKeys(hotkeys) diff --git a/accessibility/patches/itemmenu.lua b/accessibility/patches/itemmenu.lua new file mode 100644 index 0000000..0793c45 --- /dev/null +++ b/accessibility/patches/itemmenu.lua @@ -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 diff --git a/accessibility/states/_itemgrid.lua b/accessibility/states/_itemgrid.lua index 3acb796..6117cbf 100644 --- a/accessibility/states/_itemgrid.lua +++ b/accessibility/states/_itemgrid.lua @@ -7,6 +7,18 @@ -- 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") @@ -15,9 +27,71 @@ return function(options) 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. @@ -26,12 +100,6 @@ return function(options) return nil, nil end - local window = menus.getAnyUpvalue(stateTable, windowName) - - if type(window) ~= "table" or not window.getItemOnCursor then - return nil, nil - end - local position = window.cursorPosition if type(position) ~= "number" then return nil, nil @@ -59,6 +127,9 @@ return function(options) 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 @@ -68,10 +139,15 @@ return function(options) 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, } diff --git a/accessibility/testbed.lua b/accessibility/testbed.lua index c6d4f33..0f9740b 100644 --- a/accessibility/testbed.lua +++ b/accessibility/testbed.lua @@ -435,6 +435,35 @@ local function scenario() 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 -- 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"