157 lines
4.3 KiB
Lua
157 lines
4.3 KiB
Lua
-- 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
|