-- 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