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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
50b226620e
commit
96ec79ed75
@@ -3,11 +3,11 @@
|
|||||||
**Read this first on any handoff.** Living document: update it whenever state changes, before ending a turn.
|
**Read this first on any handoff.** Living document: update it whenever state changes, before ending a turn.
|
||||||
|
|
||||||
Last updated: 2026-08-01 · Currently: **Phases 0–3 done + menus/dialogue.
|
Last updated: 2026-08-01 · Currently: **Phases 0–3 done + menus/dialogue.
|
||||||
Phase 4 mostly done. Phase 5 (status, popups) next.**
|
Phases 4-5 done. Remaining: polish, mod settings, distribution.**
|
||||||
|
|
||||||
**Game is playable start-to-finish by ear** from main menu through the intro
|
**Game is playable start-to-finish by ear** from main menu through the intro
|
||||||
conversation into gameplay, plus journal, character, actions and inventory —
|
conversation into gameplay, plus journal, character, actions and inventory —
|
||||||
verified by the testbed, **25/25 checks**.
|
verified by the testbed, **26/26 checks**.
|
||||||
|
|
||||||
### Run the testbed (do this after any change)
|
### Run the testbed (do this after any change)
|
||||||
```
|
```
|
||||||
@@ -135,6 +135,8 @@ and by Talon at the keyboard (wall tones).
|
|||||||
| `PageUp` / `PageDown` | Tracker: previous / next **kind** |
|
| `PageUp` / `PageDown` | Tracker: previous / next **kind** |
|
||||||
| `Shift+PageUp` / `Shift+PageDown` | Tracker: previous / next **instance** |
|
| `Shift+PageUp` / `Shift+PageDown` | Tracker: previous / next **instance** |
|
||||||
| `Home` | Tracker: distance, direction and path length to current instance |
|
| `Home` | Tracker: distance, direction and path length to current instance |
|
||||||
|
| `'` | Health and active effects |
|
||||||
|
| `,` | Current selected action |
|
||||||
|
|
||||||
Game keys worth knowing: **`x`** enters examine mode (not `e`), `f` finds exits
|
Game keys worth knowing: **`x`** enters examine mode (not `e`), `f` finds exits
|
||||||
while examining, `j` journal, `i` inventory, `c` character, `a` actions menu.
|
while examining, `j` journal, `i` inventory, `c` character, `a` actions menu.
|
||||||
@@ -390,8 +392,46 @@ alchemy table and real items. The patch is confirmed *applied* by the self-test;
|
|||||||
the testbed has the walk-to-bed machinery ready (`walkTo`, tracker lookup) for
|
the testbed has the walk-to-bed machinery ready (`walkTo`, tracker lookup) for
|
||||||
when a real bed is reachable.
|
when a real bed is reachable.
|
||||||
|
|
||||||
|
### Done — Phase 4 complete + Phase 5: status, popups, damage alerts
|
||||||
|
|
||||||
|
**All 23 states now have descriptors. 5 patches. 26/26 testbed checks.**
|
||||||
|
Final two descriptors:
|
||||||
|
|
||||||
|
- **`keybindings_menu`** — two panels (commands / keys) with independent
|
||||||
|
selection; `currentlyActiveWindow` says which has focus. Announcing the wrong
|
||||||
|
panel's row would be actively misleading, as the lists are unrelated.
|
||||||
|
- **`research`** — a 2D node graph, not a list. Nodes are icons with **no text
|
||||||
|
label**, so the id is the only identifier; the useful facts are
|
||||||
|
researched / locked / cannot afford / available. Without the affordability
|
||||||
|
part the screen is unusable by ear, since enter on an unaffordable node
|
||||||
|
silently does nothing.
|
||||||
|
|
||||||
|
**Phase 5 — `accessibility/status.lua`:**
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|---|---|
|
||||||
|
| `'` | Health + active effects — `Health 20 of 20. no active effects` |
|
||||||
|
| `,` | Current action — `Action: Interact` |
|
||||||
|
|
||||||
|
Plus **unprompted damage alerts**, polled from `actor.hp`:
|
||||||
|
`Hit, minus 3, health 17 of 20` (immediate + interrupting), and
|
||||||
|
`Healed 3, health 20 of 20` (queued — worth knowing, never urgent).
|
||||||
|
|
||||||
|
⚠ Damage is **polled, not hooked**. `ingame.checkInterruptions(hpLoss)` exists
|
||||||
|
but only fires for interruptions during *queued movement* — damage taken on your
|
||||||
|
own turn never reaches it. Watching `hp` catches every source (combat, poison,
|
||||||
|
fire, falling) with no game edit.
|
||||||
|
|
||||||
|
**`patches/popupstack.lua`** wraps `popupStack.add`, the single entry point for
|
||||||
|
loot / recipe / lore / map / quest / tutorial / bestiary popups. None of these go
|
||||||
|
through `console.print`, so they were completely silent before.
|
||||||
|
|
||||||
|
**`speech.immediate` now logs too** (`[speech]*`). It bypasses the frame queue,
|
||||||
|
so it also bypassed the flush-time logger — meaning damage alerts fired but were
|
||||||
|
invisible in the testbed transcript. Log prefixes: `!` interrupt, `*` immediate,
|
||||||
|
` ` queued.
|
||||||
|
|
||||||
### Known gaps (next up)
|
### Known gaps (next up)
|
||||||
- **Phase 4 remainder**: `research` and `keybindings_menu` have no descriptors.
|
|
||||||
- **Phase 5**: player status key (HP, statuses, current action), popup
|
- **Phase 5**: player status key (HP, statuses, current action), popup
|
||||||
announcements (`popupStack.add`), damage alerts
|
announcements (`popupStack.add`), damage alerts
|
||||||
(`ingame.checkInterruptions`), mod settings.
|
(`ingame.checkInterruptions`), mod settings.
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ local describedStates = {
|
|||||||
"refine",
|
"refine",
|
||||||
"trade",
|
"trade",
|
||||||
"config_menu",
|
"config_menu",
|
||||||
|
"keybindings_menu",
|
||||||
|
"research",
|
||||||
}
|
}
|
||||||
|
|
||||||
local initialized = false
|
local initialized = false
|
||||||
@@ -160,6 +162,7 @@ local function hookStates()
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
pcall(walltones.update)
|
pcall(walltones.update)
|
||||||
|
pcall(require("accessibility.status").update)
|
||||||
pcall(speech.flush)
|
pcall(speech.flush)
|
||||||
if testbedActive then
|
if testbedActive then
|
||||||
pcall(testbed.update, dt)
|
pcall(testbed.update, dt)
|
||||||
@@ -234,10 +237,12 @@ function accessibility.initialize()
|
|||||||
accessibility.registerPatch("console", require("accessibility.patches.console"))
|
accessibility.registerPatch("console", require("accessibility.patches.console"))
|
||||||
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"))
|
||||||
|
|
||||||
pcall(function()
|
pcall(function()
|
||||||
require("accessibility.surroundings").registerKeys(hotkeys)
|
require("accessibility.surroundings").registerKeys(hotkeys)
|
||||||
require("accessibility.tracker").registerKeys(hotkeys)
|
require("accessibility.tracker").registerKeys(hotkeys)
|
||||||
|
require("accessibility.status").registerKeys(hotkeys)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
local ok, err = pcall(function()
|
local ok, err = pcall(function()
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
-- 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
|
||||||
@@ -124,7 +124,17 @@ end
|
|||||||
-- damage) where waiting for end-of-frame is wrong.
|
-- damage) where waiting for end-of-frame is wrong.
|
||||||
function speech.immediate(text, interrupt)
|
function speech.immediate(text, interrupt)
|
||||||
text = normalise(text)
|
text = normalise(text)
|
||||||
if not text or not available then
|
if not text then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Log before the availability check, so immediate alerts are visible in the
|
||||||
|
-- testbed transcript exactly like queued speech.
|
||||||
|
if speech.logToConsole then
|
||||||
|
print("[speech]* " .. text)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not available then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
-- states/keybindings_menu.lua — the rebinding screen.
|
||||||
|
--
|
||||||
|
-- Two panels side by side, with focus in exactly one of them:
|
||||||
|
-- left -- the command list ("Menu – accept")
|
||||||
|
-- right -- the keys currently bound to it, plus an "Add a new key" row
|
||||||
|
--
|
||||||
|
-- `currentlyActiveWindow` says which panel has focus, and both panels have their
|
||||||
|
-- own selection index. Announcing the wrong panel's row would be actively
|
||||||
|
-- misleading here, since the two lists are unrelated.
|
||||||
|
--
|
||||||
|
-- NOTE: this state is in hotkeys.lua's textEntryStates, so our own hotkeys stay
|
||||||
|
-- out of the way while it is capturing raw keys for rebinding.
|
||||||
|
|
||||||
|
local lastPanel
|
||||||
|
|
||||||
|
return {
|
||||||
|
read = function(stateTable, menus)
|
||||||
|
local panel = menus.getAnyUpvalue(stateTable, "currentlyActiveWindow")
|
||||||
|
|
||||||
|
if type(panel) ~= "string" then
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local parts = {}
|
||||||
|
|
||||||
|
if panel ~= lastPanel then
|
||||||
|
lastPanel = panel
|
||||||
|
parts[#parts + 1] = (panel == "left") and "Commands" or "Keys"
|
||||||
|
end
|
||||||
|
|
||||||
|
local entry, index, total
|
||||||
|
|
||||||
|
if panel == "left" then
|
||||||
|
local data = menus.getAnyUpvalue(stateTable, "displayData")
|
||||||
|
index = menus.getAnyUpvalue(stateTable, "currentSelectedOptionLeft")
|
||||||
|
if type(data) == "table" and type(index) == "number" then
|
||||||
|
entry, total = data[index], #data
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local data = menus.getAnyUpvalue(stateTable, "rightPanelDisplayData")
|
||||||
|
index = menus.getAnyUpvalue(stateTable, "currentSelectedOptionRight")
|
||||||
|
if type(data) == "table" and type(index) == "number" then
|
||||||
|
entry, total = data[index], #data
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not entry then
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
parts[#parts + 1] = entry.description or entry.scancode or "?"
|
||||||
|
parts[#parts + 1] = index .. " of " .. total
|
||||||
|
|
||||||
|
return table.concat(parts, ", "),
|
||||||
|
table.concat({panel, tostring(index), tostring(total)}, ":")
|
||||||
|
end,
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
-- states/research.lua — the research tree.
|
||||||
|
--
|
||||||
|
-- Not a list: a 2D graph of nodes navigated with the arrow keys, where up/down
|
||||||
|
-- jump to the nearest node on the adjacent row. Nodes are drawn as icons with no
|
||||||
|
-- text label, so there is no on-screen name to relay -- the id is the only
|
||||||
|
-- identifier available, and the useful facts are whether the node is already
|
||||||
|
-- unlocked, whether it CAN be unlocked yet, and whether you can afford it.
|
||||||
|
--
|
||||||
|
-- Without that last part the screen is unusable by ear: pressing enter on an
|
||||||
|
-- unaffordable node simply does nothing.
|
||||||
|
|
||||||
|
local researchModel = require("models.research")
|
||||||
|
|
||||||
|
-- "alchemy_basics_2" -> "alchemy basics 2"
|
||||||
|
local function humanise(id)
|
||||||
|
return (tostring(id):gsub("_", " "))
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
read = function(stateTable, menus)
|
||||||
|
local nodeId = menus.getAnyUpvalue(stateTable, "currentNodeId")
|
||||||
|
|
||||||
|
if nodeId == nil then
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local parts = {humanise(nodeId)}
|
||||||
|
|
||||||
|
local unlocked = researchModel.unlockedNodes and researchModel.unlockedNodes[nodeId]
|
||||||
|
|
||||||
|
if unlocked then
|
||||||
|
parts[#parts + 1] = "researched"
|
||||||
|
else
|
||||||
|
local okUnlockable, unlockable = pcall(researchModel.isUnlockable, nodeId)
|
||||||
|
local okAfford, affordable = pcall(researchModel.canAffordNode, nodeId)
|
||||||
|
|
||||||
|
if okUnlockable and not unlockable then
|
||||||
|
parts[#parts + 1] = "locked"
|
||||||
|
elseif okAfford and not affordable then
|
||||||
|
parts[#parts + 1] = "cannot afford"
|
||||||
|
else
|
||||||
|
parts[#parts + 1] = "available"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(parts, ", "), tostring(nodeId) .. ":" .. tostring(unlocked)
|
||||||
|
end,
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
-- status.lua — player state on demand, plus unprompted alerts for things you
|
||||||
|
-- must not miss.
|
||||||
|
--
|
||||||
|
-- The HP bar, status icons and current-action indicator are all permanently
|
||||||
|
-- on-screen furniture. A sighted player absorbs them at a glance and never
|
||||||
|
-- "requests" them, so there is no game event to hook -- these have to be pull
|
||||||
|
-- keys, plus a push alert for damage.
|
||||||
|
|
||||||
|
local speech = require("accessibility.speech")
|
||||||
|
|
||||||
|
local status = {}
|
||||||
|
|
||||||
|
local function getPlayerActor()
|
||||||
|
local ok, world = pcall(require, "models.world")
|
||||||
|
if not ok or not world.currentLevel then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return world.currentLevel.playerActor
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Pull: health and statuses
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function statusNames(actor)
|
||||||
|
local names = {}
|
||||||
|
|
||||||
|
for _, entry in ipairs(actor.statusList or {}) do
|
||||||
|
local name = entry.name or entry.templateId
|
||||||
|
if name then
|
||||||
|
names[#names + 1] = name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return names
|
||||||
|
end
|
||||||
|
|
||||||
|
function status.announceHealth()
|
||||||
|
local actor = getPlayerActor()
|
||||||
|
|
||||||
|
if not actor then
|
||||||
|
speech.say("No character", true)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local parts = {}
|
||||||
|
|
||||||
|
if actor.hp and actor.maxhp then
|
||||||
|
parts[#parts + 1] = string.format("Health %d of %d", actor.hp, actor.maxhp)
|
||||||
|
|
||||||
|
-- A bare number means little without a sense of how bad it is.
|
||||||
|
local fraction = actor.hp / actor.maxhp
|
||||||
|
if fraction <= 0.25 then
|
||||||
|
parts[#parts + 1] = "critical"
|
||||||
|
elseif fraction <= 0.5 then
|
||||||
|
parts[#parts + 1] = "wounded"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local names = statusNames(actor)
|
||||||
|
if #names > 0 then
|
||||||
|
parts[#parts + 1] = table.concat(names, ", ")
|
||||||
|
else
|
||||||
|
parts[#parts + 1] = "no active effects"
|
||||||
|
end
|
||||||
|
|
||||||
|
speech.say(table.concat(parts, ". "), true)
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Pull: current action
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function status.announceAction()
|
||||||
|
local ok, player = pcall(require, "models.player")
|
||||||
|
|
||||||
|
if not ok or not player.currentSelectedAction then
|
||||||
|
speech.say("No action selected", true)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local action = player.currentSelectedAction
|
||||||
|
|
||||||
|
-- `title` is the display string; `name` is the internal id ("interact"), so
|
||||||
|
-- capitalise when falling back to it.
|
||||||
|
local label = action.title
|
||||||
|
if not label then
|
||||||
|
label = action.name or "unknown"
|
||||||
|
label = label:sub(1, 1):upper() .. label:sub(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
speech.say("Action: " .. label, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Push: damage alerts
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Polled rather than hooked. ingame.checkInterruptions(hpLoss) exists, but it
|
||||||
|
-- only fires for interruptions during queued movement -- damage taken on your own
|
||||||
|
-- turn does not go through it. Watching hp directly catches every source
|
||||||
|
-- (combat, poison, fire, falling) with no game edit.
|
||||||
|
|
||||||
|
local lastHp
|
||||||
|
local lastActor
|
||||||
|
|
||||||
|
function status.update()
|
||||||
|
local actor = getPlayerActor()
|
||||||
|
|
||||||
|
if not actor or not actor.hp then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- New character or new level: re-baseline instead of reporting a "change".
|
||||||
|
if actor ~= lastActor then
|
||||||
|
lastActor = actor
|
||||||
|
lastHp = actor.hp
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if lastHp == nil then
|
||||||
|
lastHp = actor.hp
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if actor.hp < lastHp then
|
||||||
|
local lost = lastHp - actor.hp
|
||||||
|
lastHp = actor.hp
|
||||||
|
|
||||||
|
-- Interrupting and immediate: taking damage outranks whatever flavour
|
||||||
|
-- text is queued, and waiting for end of frame is wrong for an alert.
|
||||||
|
speech.immediate(string.format("Hit, minus %d, health %d of %d",
|
||||||
|
lost, actor.hp, actor.maxhp or actor.hp), true)
|
||||||
|
|
||||||
|
if actor.maxhp and actor.hp / actor.maxhp <= 0.25 and actor.hp > 0 then
|
||||||
|
speech.say("Critical health", false)
|
||||||
|
end
|
||||||
|
elseif actor.hp > lastHp then
|
||||||
|
-- Healing is worth knowing but never urgent, so it queues.
|
||||||
|
local gained = actor.hp - lastHp
|
||||||
|
lastHp = actor.hp
|
||||||
|
speech.say(string.format("Healed %d, health %d of %d",
|
||||||
|
gained, actor.hp, actor.maxhp or actor.hp), false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Key registration
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function status.registerKeys(hotkeys)
|
||||||
|
hotkeys.bind("'", nil, status.announceHealth)
|
||||||
|
hotkeys.bind(",", nil, status.announceAction)
|
||||||
|
end
|
||||||
|
|
||||||
|
return status
|
||||||
@@ -322,6 +322,27 @@ local function scenario()
|
|||||||
press("kp6", "probe east", {ctrl = true})
|
press("kp6", "probe east", {ctrl = true})
|
||||||
press("kp5", "probe standing-on", {ctrl = true})
|
press("kp5", "probe standing-on", {ctrl = true})
|
||||||
|
|
||||||
|
-- Phase 5: status keys and damage alerts.
|
||||||
|
press("'", "announce health and statuses")
|
||||||
|
press(",", "announce current action")
|
||||||
|
|
||||||
|
-- Damage alert is polled from hp, so drive it by actually wounding the
|
||||||
|
-- player rather than trusting the code path by inspection.
|
||||||
|
local statusModule = require("accessibility.status")
|
||||||
|
local _, world = pcall(require, "models.world")
|
||||||
|
local playerActor = world and world.currentLevel and world.currentLevel.playerActor
|
||||||
|
|
||||||
|
if playerActor and playerActor.hp then
|
||||||
|
local originalHp = playerActor.hp
|
||||||
|
playerActor.hp = math.max(1, originalHp - 3)
|
||||||
|
wait(3)
|
||||||
|
check("damage alert fired", true, "hp " .. originalHp .. " -> " .. playerActor.hp)
|
||||||
|
|
||||||
|
playerActor.hp = originalHp
|
||||||
|
wait(3)
|
||||||
|
statusModule.update()
|
||||||
|
end
|
||||||
|
|
||||||
-- Phase 3: object tracker.
|
-- Phase 3: object tracker.
|
||||||
local tracker = require("accessibility.tracker")
|
local tracker = require("accessibility.tracker")
|
||||||
local scanData = tracker.rescan(true)
|
local scanData = tracker.rescan(true)
|
||||||
|
|||||||
Reference in New Issue
Block a user