Files
Accessibility ModandClaude Opus 5 96ec79ed75 Phase 4 complete + Phase 5: status, popups, damage alerts
Game-file footprint unchanged: still one line in main.lua.
All 23 states now have descriptors; 5 patches applied.

Final two descriptors:

- keybindings_menu has two panels (commands and keys) with independent
  selection, so it reads currentlyActiveWindow to decide which one has focus.
  Announcing the wrong panel's row would be actively misleading, since the two
  lists are unrelated.
- research is a 2D node graph rather than a list, and its nodes are drawn as
  icons with no text label, so the id is the only identifier available. What
  actually matters is researched / locked / cannot afford / available: without
  the affordability part the screen is unusable by ear, because pressing enter
  on an unaffordable node silently does nothing.

status.lua adds two pull keys -- ' for health and active effects, , for the
current action -- plus unprompted damage alerts.

Damage is polled from actor.hp rather than hooked. ingame.checkInterruptions
exists but only fires for interruptions during queued movement, so damage taken
on your own turn never reaches it. Polling catches every source (combat, poison,
fire, falling) with no game edit. Damage speaks immediately and interrupts;
healing queues, since it is worth knowing but never urgent.

patches/popupstack.lua wraps popupStack.add, the single entry point for loot,
recipe, lore, map, quest, tutorial and bestiary popups. None of these go through
console.print, so they were entirely silent before.

speech.immediate now logs to the console like queued speech does. It bypasses
the frame queue and so also bypassed the flush-time logger, which meant damage
alerts fired correctly but were invisible in the testbed transcript. Prefixes
are now: ! interrupt, * immediate, space queued.

26/26 testbed checks, history unit tests passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:20:50 +02:00

81 lines
2.3 KiB
Lua

-- Patch: views/popupStack.lua
--
-- Popups are the game's way of telling you something happened without
-- interrupting play: loot found, a recipe learned, a quest updated, a new beast
-- recorded, a tutorial offered. They fade in at the side of the screen and none
-- of them go through console.print, so without this they are entirely silent.
--
-- popupStack.add{...} is the single entry point for all of them.
local speech = require("accessibility.speech")
-- What each popup type is called out loud. Keys match the `popupType` field
-- passed by states/ingame.lua and states/ingame_observe.lua.
local typeNames = {
loot = "Found",
recipe = "Recipe",
lore = "Lore",
map = "Map",
journal = "Quest",
tutorial = "Tutorial",
bestiary = "Bestiary",
}
local function describe(params)
local parts = {}
parts[#parts + 1] = typeNames[params.popupType] or params.popupType or "Notice"
-- `header` is the human-readable line the popup itself displays
-- ("You found:", a quest title), so prefer it over any id.
if params.header and params.header ~= "" then
parts[#parts + 1] = params.header
end
if params.loot then
for _, entry in ipairs(params.loot) do
local name = (type(entry) == "table") and (entry.name or entry.templateId) or tostring(entry)
if name then
local quantity = type(entry) == "table" and entry.quantity
parts[#parts + 1] = (quantity and quantity > 1)
and (name .. " x" .. quantity) or name
end
end
end
if params.quest and type(params.quest) == "table" and params.quest.title then
parts[#parts + 1] = params.quest.title
end
if params.templateId and not params.header then
parts[#parts + 1] = (tostring(params.templateId):gsub("_", " "))
end
return table.concat(parts, ": ", 1, math.min(2, #parts))
.. (#parts > 2 and (". " .. table.concat(parts, ", ", 3)) or "")
end
return function()
local popupStack = require("views.popupStack")
local realAdd = popupStack.add
if not realAdd then
error("views.popupStack has no add function -- game structure changed")
end
popupStack.add = function(params)
local result = realAdd(params)
pcall(function()
if type(params) ~= "table" then
return
end
-- Queued, not interrupting: a popup is informational, and several can
-- land in one turn (loot plus a quest update).
speech.say(describe(params), false)
end)
return result
end
end