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
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user