-- 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(). -- Third thing this owns: DETAILS (the `=` key and the settle timer, see -- accessibility/details.lua). Two sources, both already composed by the game: -- -- * item.getPopupMessage() -- description, category, condition, refine yields, -- lore progress. Read through accessibility/itemdetails.lua. -- * the HINT PANEL that craft, refine and trade each draw permanently beside -- the grid, and which the cursor popup never contains. Notably PRICE exists -- only there (states/trade.lua:156), as do a recipe's component costs and -- whether you can afford them. -- -- The three panels turn out to share a shape -- hintDescription, a componentHints -- list of {nameText, descriptionText, color}, and a budget line -- so one reader -- covers all three rather than three per-state callbacks. Fields a given screen -- doesn't use are simply nil (craft has no budget line; trade has no -- hintDescription; refine names an output instead of components). local itemCategories = require("modules.item.categories") local itemdetails = require("accessibility.itemdetails") -- The game marks an unaffordable component by colouring it red. That is the one -- fact which decides whether pressing enter does anything at all, so it has to be -- spoken -- and the colour is the only place it survives into the hint table. local function isUnaffordableColor(color) return type(color) == "table" and color[1] and color[1] > 0.8 and color[2] and color[2] < 0.5 end -- craft / refine / trade draw a permanent hint panel beside the grid. local function hintBlocks(stateTable, menus) local blocks = {} local function add(text) if type(text) == "string" then text = itemdetails.flatten(text) if text ~= "" then blocks[#blocks + 1] = text end end end -- craft: the recipe's brewing instructions. refine: how this refinement works. add(menus.getAnyUpvalue(stateTable, "hintDescription")) -- refine: what you get out, and how much of it you already have. local outputName = menus.getAnyUpvalue(stateTable, "outputName") if outputName then local quantity = menus.getAnyUpvalue(stateTable, "outputQuantityText") add("Yields " .. outputName .. (quantity and (", " .. quantity) or "")) end -- craft: one per recipe component. trade: the price, as a single "component". local componentHints = menus.getAnyUpvalue(stateTable, "componentHints") if type(componentHints) == "table" then for _, hint in ipairs(componentHints) do if type(hint) == "table" and hint.nameText then add("Costs " .. hint.nameText .. (hint.descriptionText and (", " .. hint.descriptionText) or "") .. (isUnaffordableColor(hint.color) and ", cannot afford" or "")) end end end -- refine / trade: "You already have: x2.00 green paste". local budgetName = menus.getAnyUpvalue(stateTable, "budgetNameText") if budgetName then local budgetDescription = menus.getAnyUpvalue(stateTable, "budgetDescription") local budgetQuantity = menus.getAnyUpvalue(stateTable, "budgetQuantityText") add((budgetDescription or "You have:") .. " " .. (budgetQuantity or "") .. " " .. budgetName) end return blocks end 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 -- The window closure, or nil if the screen isn't built yet. local function getWindow(stateTable, menus) local window = menus.getAnyUpvalue(stateTable, windowName) if type(window) ~= "table" or not window.getItemOnCursor then return nil end return window end return { reset = function() lastTab = nil lastMenu = nil end, -- Everything the focus announcement is too terse to say. See details.lua. details = function(stateTable, menus, verbose) local window = getWindow(stateTable, menus) if not window then return nil end -- The slider owns speech during quantity selection (patches/slider.lua); -- reading a description over the number being adjusted would fight it. if menus.getAnyUpvalue(stateTable, "confirmationWindowEnabled") then return nil end -- Note the item action menu is NOT excluded here, unlike in read(). The -- menu is modal over the grid so the highlighted VERB is what read() -- announces, but the thing you want described is still the item the menu -- belongs to -- and that is exactly when you want to know what "Drink" -- would be drinking. local ok, item = pcall(window.getItemOnCursor) if not ok or not item then return nil end local blocks = itemdetails.blocks(item, verbose) or {} for _, block in ipairs(hintBlocks(stateTable, menus)) do blocks[#blocks + 1] = block end if #blocks == 0 then return nil end return blocks end, read = function(stateTable, menus) local window = getWindow(stateTable, menus) if not window 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