Prompted by "do sighted players have a minimap we don't have access to?"
No widget, but two map surfaces were only half exposed: the world map
screen, and the level view itself, which IS the explored-floor map --
rememberedFoV keeps everything ever seen drawn, and a sighted player pans
over it and reads the fog edge at a glance.
The world map dropped the three things the picture carries: what kind of
place each area is (the icon shape), which way it lies, and how long
travelling there takes. The last is the number the decision turns on and
was only ever in the side panel. states/worldmap.lua is now a read() plus
details() rather than probe/render -- necessarily, because render() gets
no state table and the travel time needs the screen's currentAreaId, an
upvalue like options and selectedOption. Travel time is the game's own
getDistanceMessage, the same call the screen makes. The blurb runs to
several sentences, so it goes on = and the settle timer instead of the
focus line.
Two gotchas: options[i].icon is already an image object, so the icon id
string has to come off the model area; and describe.direction only tests
the sign of each delta, which on the map's floating-point coordinates
called almost everything diagonal, hence a 25% deadzone carrying the same
4/3 x-correction the game's own distance maths uses.
New frontier.lua answers "where haven't I been", which nothing did:
surroundings is local, examine is one tile, and the tracker only finds
things it already knows about. A frontier tile is seen, not blocking
movement, with an in-bounds unseen CARDINAL neighbour -- cardinal because
the diagonal test fires on the unseen tile behind every wall corner, so a
lit corridor would report an unexplored way per corner. Clustered
8-connected and reduced to each cluster's nearest member, so a wide
doorway is one way but two openings that both lie north stay two. Reads
only FoV/rememberedFoV and reports only THAT an opening exists, never
what is beyond it -- exactly what the fog edge shows.
Shift+End names the nearest exit without disturbing the tracker
selection. Note for later: level.exits does not exist at runtime, it is
packed as nil and never assigned. The primitive is the game's own
getListByTag("exit"), which already gates on FoV or rememberedFoV, skips
hidden tiles and sorts by distance. Exits were already a tracker
category, so this is a shortcut rather than a new capability.
"remembered" moves from verbose to normal verbosity. tileFacts reads
actors only when a tile is currently VISIBLE, so a remembered tile is
silent about whatever may be standing on it now, and at normal verbosity
that silence was indistinguishable from "nothing there". A safety flag,
not a nicety.
Also adds descriptor.probeNames: a read() descriptor has no probe table,
so menus.selfTest could only report "custom read()" and the version-bump
canary was lost on exactly the screens reaching for the most upvalues.
worldmap and research now declare their game locals and get the same
PROBE BROKEN check.
120 testbed checks, 0 failures. It caught two things review had not. The
world map said "You are currently here." twice, because
getDistanceMessage and getLastVisitedMessage return the same sentence for
the area you are standing in -- fine as two panels on screen, a stutter
in one utterance. And the first frontier check walked a step east and
demanded the numbers change; it failed on a run where nothing was wrong,
the step landing but both openings keeping the same Chebyshev distance.
Replaced with the deterministic property: every reported distance must
equal Chebyshev from the player's current position, nearest first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
655 lines
19 KiB
Lua
655 lines
19 KiB
Lua
-- tracker.lua — the object tracker.
|
|
--
|
|
-- Three-level navigation (docs/05-decisions.md D2):
|
|
--
|
|
-- Ctrl+PageUp / Ctrl+PageDown category "Creatures"
|
|
-- PageUp / PageDown kind "rat"
|
|
-- Shift+PageUp / Shift+PageDown instance that specific rat
|
|
-- Home announce distance + direction
|
|
-- Ctrl+Home walk there (what a left click does)
|
|
-- Shift+End nearest known exit, without touching the above
|
|
--
|
|
-- Two hard rules:
|
|
--
|
|
-- * Never report anything outside FoV / rememberedFoV. The tracker must not
|
|
-- become a cheat -- it exposes what a sighted player could see, no more.
|
|
-- * Never scan per frame. A scan walks xMax x yMax; it runs on keypress and is
|
|
-- cached until the world changes under it.
|
|
|
|
local speech = require("accessibility.speech")
|
|
local describe = require("accessibility.describe")
|
|
local categories = require("accessibility.categories")
|
|
|
|
local tracker = {}
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- State
|
|
--------------------------------------------------------------------------------
|
|
|
|
local categoryIndex = 1
|
|
local kindIndex = 1
|
|
local instanceIndex = 1
|
|
|
|
local scan -- { [categoryKey] = { kinds = { {key, name, instances={{x,y,ref},..}}, .. } } }
|
|
local scanSignature
|
|
|
|
-- What the player last deliberately selected, so a rescan can put the cursor back
|
|
-- on it. Indices alone are useless here: the lists are distance-sorted, so a
|
|
-- single step re-orders them and a bare index silently selects a different rat.
|
|
local selectedKindKey -- string, or nil
|
|
local selectedInstance -- {ref = actor} for creatures, {x, y} for tiles
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Scanning
|
|
--------------------------------------------------------------------------------
|
|
|
|
local function getLevel()
|
|
local ok, world = pcall(require, "models.world")
|
|
if not ok or not world.currentLevel then
|
|
return nil
|
|
end
|
|
return world.currentLevel
|
|
end
|
|
|
|
-- Cheap staleness key. Position matters because everything is sorted by distance
|
|
-- to the player; turn matters because creatures move and containers empty.
|
|
local function signatureFor(level)
|
|
local actor = level.playerActor
|
|
local turn = level.world and level.world.turn or 0
|
|
return string.format("%s:%s:%s:%s",
|
|
tostring(level.name), tostring(actor and actor.x), tostring(actor and actor.y), tostring(turn))
|
|
end
|
|
|
|
local function seenAt(level, x, y)
|
|
if level.FoV and level.FoV[x] and level.FoV[x][y] then
|
|
return true, true
|
|
end
|
|
if level.rememberedFoV and level.rememberedFoV[x] and level.rememberedFoV[x][y] then
|
|
return true, false
|
|
end
|
|
return false, false
|
|
end
|
|
|
|
local function newBucket()
|
|
return {kinds = {}, byKind = {}}
|
|
end
|
|
|
|
-- ref, when given, is the actor table itself: a creature's identity survives it
|
|
-- walking, its coordinates do not.
|
|
local function addTo(bucket, kindKey, kindName, x, y, ref)
|
|
if not kindKey then
|
|
return
|
|
end
|
|
|
|
local kind = bucket.byKind[kindKey]
|
|
|
|
if not kind then
|
|
kind = {key = kindKey, name = kindName, instances = {}}
|
|
bucket.byKind[kindKey] = kind
|
|
bucket.kinds[#bucket.kinds + 1] = kind
|
|
end
|
|
|
|
kind.instances[#kind.instances + 1] = {x = x, y = y, ref = ref}
|
|
end
|
|
|
|
function tracker.rescan(force)
|
|
local level = getLevel()
|
|
if not level or not level.playerActor then
|
|
scan = nil
|
|
return nil
|
|
end
|
|
|
|
local signature = signatureFor(level)
|
|
if not force and scan and signature == scanSignature then
|
|
return scan
|
|
end
|
|
|
|
local buckets = {}
|
|
for _, category in ipairs(categories.list) do
|
|
buckets[category.key] = newBucket()
|
|
end
|
|
|
|
local px, py = level.playerActor.x, level.playerActor.y
|
|
|
|
-- Creatures come from the actor list, not the grid.
|
|
for _, actor in ipairs(level.actors or {}) do
|
|
if actor ~= level.playerActor
|
|
and actor.x and actor.y
|
|
and actor.visibilityAlpha ~= 0
|
|
then
|
|
local visible = level.FoV and level.FoV[actor.x] and level.FoV[actor.x][actor.y]
|
|
if visible then
|
|
addTo(buckets.creatures, categories.actorKind(actor),
|
|
categories.actorName(actor), actor.x, actor.y, actor)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Tiles.
|
|
for x = 1, level.xMax or 0 do
|
|
if level.grid[x] then
|
|
for y = 1, level.yMax or 0 do
|
|
local stack = level.grid[x][y]
|
|
if stack and seenAt(level, x, y) then
|
|
local claimed = {}
|
|
|
|
for i = #stack, 1, -1 do
|
|
local tile = stack[i]
|
|
local key = categories.classifyTile(tile)
|
|
|
|
-- One entry per category per cell: a cell can hold both a
|
|
-- door and a floor, but not two doors worth reporting.
|
|
if key and not claimed[key] then
|
|
claimed[key] = true
|
|
addTo(buckets[key], categories.tileKind(tile),
|
|
categories.tileName(tile), x, y)
|
|
end
|
|
end
|
|
|
|
if not claimed.terrain then
|
|
local ok, isWater = pcall(level.isWaterTile, x, y)
|
|
if ok and isWater then
|
|
addTo(buckets.terrain, "water", "water", x, y)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Sort instances by distance to the player, and kinds by their nearest
|
|
-- instance, so both levels of navigation move outward.
|
|
local function chebyshev(a)
|
|
return math.max(math.abs(a.x - px), math.abs(a.y - py))
|
|
end
|
|
|
|
for _, bucket in pairs(buckets) do
|
|
for _, kind in ipairs(bucket.kinds) do
|
|
table.sort(kind.instances, function(a, b)
|
|
return chebyshev(a) < chebyshev(b)
|
|
end)
|
|
end
|
|
table.sort(bucket.kinds, function(a, b)
|
|
return chebyshev(a.instances[1]) < chebyshev(b.instances[1])
|
|
end)
|
|
end
|
|
|
|
scan = buckets
|
|
scanSignature = signature
|
|
tracker.reanchor()
|
|
return scan
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Selection stability
|
|
--
|
|
-- The scan is rebuilt from scratch every turn and both lists are distance-sorted,
|
|
-- so the same rat is at a different index each time it or the player moves. We
|
|
-- therefore track the selection by identity and re-derive the indices after every
|
|
-- rescan. The selection is only dropped when the thing genuinely stops being
|
|
-- reported -- out of FoV, killed, picked up.
|
|
--------------------------------------------------------------------------------
|
|
|
|
local function sameInstance(instance, wanted)
|
|
if not instance or not wanted then
|
|
return false
|
|
end
|
|
if wanted.ref then
|
|
return instance.ref == wanted.ref
|
|
end
|
|
return instance.ref == nil and instance.x == wanted.x and instance.y == wanted.y
|
|
end
|
|
|
|
-- Record what is selected right now. Called after every deliberate move of the
|
|
-- selection, never during a rescan.
|
|
local function remember()
|
|
local category = categories.list[categoryIndex]
|
|
local bucket = scan and category and scan[category.key]
|
|
local kind = bucket and bucket.kinds[kindIndex]
|
|
|
|
selectedKindKey = kind and kind.key or nil
|
|
|
|
local instance = kind and kind.instances[instanceIndex]
|
|
if instance then
|
|
selectedInstance = instance.ref and {ref = instance.ref} or {x = instance.x, y = instance.y}
|
|
else
|
|
selectedInstance = nil
|
|
end
|
|
end
|
|
|
|
function tracker.reanchor()
|
|
local category = categories.list[categoryIndex]
|
|
local bucket = scan and category and scan[category.key]
|
|
|
|
if not bucket or not selectedKindKey then
|
|
return
|
|
end
|
|
|
|
local foundKind
|
|
for i, kind in ipairs(bucket.kinds) do
|
|
if kind.key == selectedKindKey then
|
|
kindIndex, foundKind = i, kind
|
|
break
|
|
end
|
|
end
|
|
|
|
if not foundKind then
|
|
-- The kind is gone from view entirely. Let the clamp in currentKind pick
|
|
-- whatever is nearest and stop pretending we still have a selection.
|
|
selectedKindKey, selectedInstance = nil, nil
|
|
return
|
|
end
|
|
|
|
if not selectedInstance then
|
|
return
|
|
end
|
|
|
|
for i, instance in ipairs(foundKind.instances) do
|
|
if sameInstance(instance, selectedInstance) then
|
|
instanceIndex = i
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Kind still visible, this particular one is not.
|
|
selectedInstance = nil
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Access
|
|
--------------------------------------------------------------------------------
|
|
|
|
local function currentCategory()
|
|
return categories.list[categoryIndex]
|
|
end
|
|
|
|
local function currentBucket()
|
|
local data = tracker.rescan()
|
|
local category = currentCategory()
|
|
if not data or not category then
|
|
return nil
|
|
end
|
|
return data[category.key]
|
|
end
|
|
|
|
local function currentKind()
|
|
local bucket = currentBucket()
|
|
if not bucket or #bucket.kinds == 0 then
|
|
return nil
|
|
end
|
|
kindIndex = math.max(1, math.min(kindIndex, #bucket.kinds))
|
|
return bucket.kinds[kindIndex], bucket
|
|
end
|
|
|
|
local function currentInstance()
|
|
local kind = currentKind()
|
|
if not kind then
|
|
return nil
|
|
end
|
|
instanceIndex = math.max(1, math.min(instanceIndex, #kind.instances))
|
|
return kind.instances[instanceIndex], kind
|
|
end
|
|
|
|
-- What is selected right now, after any pending rescan. Used by the testbed, and
|
|
-- handy for anything that wants to follow the tracker cursor.
|
|
function tracker.currentSelection()
|
|
local instance, kind = currentInstance()
|
|
if not instance then
|
|
return nil
|
|
end
|
|
return {
|
|
category = currentCategory() and currentCategory().key,
|
|
kind = kind.key,
|
|
name = kind.name,
|
|
x = instance.x,
|
|
y = instance.y,
|
|
ref = instance.ref,
|
|
}
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Announcements
|
|
--------------------------------------------------------------------------------
|
|
|
|
local function offsetFromPlayer(instance)
|
|
local level = getLevel()
|
|
local actor = level and level.playerActor
|
|
if not actor then
|
|
return ""
|
|
end
|
|
return describe.offset(instance.x - actor.x, instance.y - actor.y)
|
|
end
|
|
|
|
local function announceCategory()
|
|
local category = currentCategory()
|
|
local bucket = currentBucket()
|
|
|
|
if not category then
|
|
return
|
|
end
|
|
|
|
if not bucket or #bucket.kinds == 0 then
|
|
speech.say(category.name .. ", none", true)
|
|
return
|
|
end
|
|
|
|
local count = #bucket.kinds
|
|
speech.say(string.format("%s, %d %s", category.name, count,
|
|
count == 1 and "kind" or "kinds"), true)
|
|
end
|
|
|
|
local function announceKind()
|
|
local kind, bucket = currentKind()
|
|
|
|
if not kind then
|
|
local category = currentCategory()
|
|
speech.say((category and category.name or "Nothing") .. ", none", true)
|
|
return
|
|
end
|
|
|
|
local total = #kind.instances
|
|
local instance = kind.instances[1]
|
|
|
|
speech.say(string.format("%s, %d, nearest %s, %d of %d",
|
|
kind.name, total, offsetFromPlayer(instance), kindIndex, #bucket.kinds), true)
|
|
end
|
|
|
|
local function announceInstance()
|
|
local instance, kind = currentInstance()
|
|
|
|
if not instance then
|
|
return
|
|
end
|
|
|
|
speech.say(string.format("%s %d of %d, %s",
|
|
kind.name, instanceIndex, #kind.instances, offsetFromPlayer(instance)), true)
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Navigation
|
|
--------------------------------------------------------------------------------
|
|
|
|
local function cycle(value, delta, count)
|
|
if count == 0 then
|
|
return 1
|
|
end
|
|
value = value + delta
|
|
if value < 1 then
|
|
value = count
|
|
elseif value > count then
|
|
value = 1
|
|
end
|
|
return value
|
|
end
|
|
|
|
function tracker.cycleCategory(delta)
|
|
tracker.rescan()
|
|
categoryIndex = cycle(categoryIndex, delta, #categories.list)
|
|
kindIndex = 1
|
|
instanceIndex = 1
|
|
remember()
|
|
announceCategory()
|
|
end
|
|
|
|
function tracker.cycleKind(delta)
|
|
local bucket = currentBucket()
|
|
if not bucket or #bucket.kinds == 0 then
|
|
announceKind()
|
|
return
|
|
end
|
|
kindIndex = cycle(kindIndex, delta, #bucket.kinds)
|
|
instanceIndex = 1
|
|
remember()
|
|
announceKind()
|
|
end
|
|
|
|
function tracker.cycleInstance(delta)
|
|
local kind = currentKind()
|
|
if not kind then
|
|
announceKind()
|
|
return
|
|
end
|
|
instanceIndex = cycle(instanceIndex, delta, #kind.instances)
|
|
remember()
|
|
announceInstance()
|
|
end
|
|
|
|
-- Home: where is the current thing, and how far is it to actually walk there.
|
|
function tracker.announceLocation()
|
|
local instance, kind = currentInstance()
|
|
|
|
if not instance then
|
|
local category = currentCategory()
|
|
speech.say((category and category.name or "Tracker") .. ", nothing selected", true)
|
|
return
|
|
end
|
|
|
|
local parts = {
|
|
string.format("%s, %s", kind.name, offsetFromPlayer(instance)),
|
|
}
|
|
|
|
-- Straight-line offset can be badly misleading indoors -- a chest three tiles
|
|
-- north through a wall may be twenty steps away. Path length is the number
|
|
-- that actually matters.
|
|
local level = getLevel()
|
|
if level then
|
|
local ok, path = pcall(level.calculatePathFromPlayer, instance.x, instance.y)
|
|
if ok and path and #path > 0 then
|
|
parts[#parts + 1] = #path .. (#path == 1 and " step by path" or " steps by path")
|
|
elseif ok then
|
|
parts[#parts + 1] = "no path"
|
|
end
|
|
end
|
|
|
|
speech.say(table.concat(parts, ", "), true)
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Nearest exit
|
|
--------------------------------------------------------------------------------
|
|
--
|
|
-- Exits are already a tracker category, but reaching them costs several keypresses
|
|
-- of category cycling and clobbers whatever you were tracking. "Where is the way
|
|
-- out" is asked often enough, and under enough pressure, to deserve one key that
|
|
-- answers without disturbing the selection.
|
|
--
|
|
-- This does NOT use the tracker's own scan. The game has the exact primitive
|
|
-- already: level.getListByTag("exit") (modules/level.lua:1382) walks the grid,
|
|
-- gates on FoV or rememberedFoV, skips hidden tiles and returns the results
|
|
-- sorted by distance to the player. Reusing it means the FoV rule is enforced by
|
|
-- vanilla rather than restated here.
|
|
--
|
|
-- Note for anyone tempted by level.exits: it does not exist at runtime. It is
|
|
-- packed as nil (modules/level.lua:416) and never assigned; `exits` is a
|
|
-- generator parameter that gets baked into individual tiles.
|
|
|
|
local exitTemplates
|
|
|
|
local function getExitTemplates()
|
|
if exitTemplates then
|
|
return exitTemplates
|
|
end
|
|
|
|
local ok, templatesByTag = pcall(require, "modules.tile.templatesByTag")
|
|
exitTemplates = (ok and templatesByTag and templatesByTag.exit) or {}
|
|
return exitTemplates
|
|
end
|
|
|
|
-- getListByTag returns bare {x, y}, so the name has to come back off the stack.
|
|
local function exitNameAt(level, x, y)
|
|
local stack = level.grid and level.grid[x] and level.grid[x][y]
|
|
if type(stack) ~= "table" then
|
|
return "exit"
|
|
end
|
|
|
|
local templates = getExitTemplates()
|
|
|
|
for i = #stack, 1, -1 do
|
|
local tile = stack[i]
|
|
if tile and not tile.isHidden and templates[tile.templateId] then
|
|
return categories.tileName(tile)
|
|
end
|
|
end
|
|
|
|
return "exit"
|
|
end
|
|
|
|
-- Shift+End: the nearest way off this level.
|
|
function tracker.announceNearestExit()
|
|
local level = getLevel()
|
|
local actor = level and level.playerActor
|
|
|
|
if not level or not actor then
|
|
speech.say("No exits known", true)
|
|
return
|
|
end
|
|
|
|
local ok, list = pcall(level.getListByTag, "exit")
|
|
if not ok or type(list) ~= "table" or not list[1] then
|
|
speech.say("No exits known", true)
|
|
return
|
|
end
|
|
|
|
local nearest = list[1]
|
|
|
|
local parts = {
|
|
string.format("%s, %s", exitNameAt(level, nearest.x, nearest.y),
|
|
describe.offset(nearest.x - actor.x, nearest.y - actor.y)),
|
|
}
|
|
|
|
-- Same reasoning as announceLocation: straight-line offset is misleading
|
|
-- indoors, and the path count truncates to one step while threatened.
|
|
local okPath, path = pcall(level.calculatePathFromPlayer, nearest.x, nearest.y)
|
|
if okPath and path and #path > 0 then
|
|
parts[#parts + 1] = #path .. (#path == 1 and " step by path" or " steps by path")
|
|
elseif okPath then
|
|
parts[#parts + 1] = "no path"
|
|
end
|
|
|
|
if #list > 1 then
|
|
parts[#parts + 1] = #list .. " known"
|
|
end
|
|
|
|
speech.say(table.concat(parts, ", "), true)
|
|
end
|
|
|
|
-- Ctrl+Home: actually go there.
|
|
--
|
|
-- This is the keyboard equivalent of a left click on the tile (states/ingame.lua
|
|
-- mousepressed): build a path with calculatePathFromPlayer, then hand it to the
|
|
-- level's input buffer with the ingame state as the owner. We deliberately use
|
|
-- the game's own two-branch shape -- a one-step path goes through playerInput
|
|
-- directly, because bufferInput for a single move re-enters the buffer machinery
|
|
-- for no reason.
|
|
--
|
|
-- Most things worth tracking stand on impassable tiles (a rat, a chest), and
|
|
-- calculatePathFromPlayer refuses those outright. So when the tile itself has no
|
|
-- path we path to the best neighbouring tile instead, which is what a player
|
|
-- clicking a monster expects to happen anyway.
|
|
local function pathTo(level, x, y)
|
|
local ok, path = pcall(level.calculatePathFromPlayer, x, y)
|
|
if ok and path and #path > 0 then
|
|
return path, true
|
|
end
|
|
|
|
local best
|
|
for dx = -1, 1 do
|
|
for dy = -1, 1 do
|
|
if dx ~= 0 or dy ~= 0 then
|
|
local okNear, near = pcall(level.calculatePathFromPlayer, x + dx, y + dy)
|
|
if okNear and near and #near > 0 and (not best or #near < #best) then
|
|
best = near
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return best, false
|
|
end
|
|
|
|
function tracker.walkToSelection()
|
|
local instance, kind = currentInstance()
|
|
|
|
if not instance then
|
|
local category = currentCategory()
|
|
speech.say((category and category.name or "Tracker") .. ", nothing selected", true)
|
|
return
|
|
end
|
|
|
|
-- Walking is only meaningful on the map. In a menu or the observe cursor the
|
|
-- input buffer belongs to another state and feeding it moves would be a bug.
|
|
local okStates, states = pcall(require, "states")
|
|
if not okStates or states.getCurrent() ~= "ingame" then
|
|
speech.say("Cannot walk from here", true)
|
|
return
|
|
end
|
|
|
|
local level = getLevel()
|
|
local actor = level and level.playerActor
|
|
if not actor then
|
|
return
|
|
end
|
|
|
|
if instance.x == actor.x and instance.y == actor.y then
|
|
speech.say(kind.name .. ", already here", true)
|
|
return
|
|
end
|
|
|
|
local path, exact = pathTo(level, instance.x, instance.y)
|
|
if not path then
|
|
speech.say(kind.name .. ", no path", true)
|
|
return
|
|
end
|
|
|
|
-- Same module key states.lua uses, so we share its cached instance.
|
|
local okIngame, ingame = pcall(require, "states/ingame")
|
|
if not okIngame then
|
|
return
|
|
end
|
|
|
|
pcall(level.skipAllAnimations)
|
|
|
|
local okMove, err
|
|
if #path == 1 then
|
|
okMove, err = pcall(level.playerInput, "move",
|
|
actor.x + path[1].dx, actor.y + path[1].dy, ingame)
|
|
else
|
|
okMove, err = pcall(level.bufferInput, "move", path, ingame)
|
|
end
|
|
|
|
if not okMove then
|
|
print("[accessibility] walk to selection failed: " .. tostring(err))
|
|
speech.say(kind.name .. ", cannot walk there", true)
|
|
return
|
|
end
|
|
|
|
-- calculatePathFromPlayer truncates to a single step while the player is
|
|
-- threatened, so the spoken count is the number of steps that will actually
|
|
-- be taken, not the distance to the target.
|
|
speech.say(string.format("Walking %s %s, %d %s",
|
|
exact and "to" or "next to", kind.name,
|
|
#path, #path == 1 and "step" or "steps"), true)
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Key registration
|
|
--------------------------------------------------------------------------------
|
|
|
|
function tracker.registerKeys(hotkeys)
|
|
hotkeys.bind("pageup", "ctrl", function() tracker.cycleCategory(-1) end)
|
|
hotkeys.bind("pagedown", "ctrl", function() tracker.cycleCategory(1) end)
|
|
|
|
hotkeys.bind("pageup", "shift", function() tracker.cycleInstance(-1) end)
|
|
hotkeys.bind("pagedown", "shift", function() tracker.cycleInstance(1) end)
|
|
|
|
hotkeys.bind("pageup", nil, function() tracker.cycleKind(-1) end)
|
|
hotkeys.bind("pagedown", nil, function() tracker.cycleKind(1) end)
|
|
|
|
hotkeys.bind("home", nil, tracker.announceLocation)
|
|
hotkeys.bind("home", "ctrl", tracker.walkToSelection)
|
|
|
|
hotkeys.bind("end", "shift", tracker.announceNearestExit)
|
|
end
|
|
|
|
return tracker
|