tracker: sticky selection and Ctrl+Home walk-to; walltones: conventional volume
Tracker selection now survives a rescan. The scan is rebuilt every turn and both lists are distance-sorted, so holding a bare index meant one step by the player silently re-pointed the selection at a different rat. The tracker now remembers identity instead -- the actor table for creatures, coordinates for tiles -- and re-derives the indices in reanchor() after each rescan. Selection is dropped only when the thing genuinely stops being reported (out of FoV, killed, picked up), and the existing clamp then falls back to the nearest. Ctrl+Home walks to the selection, reusing the exact shape of the game's own click-to-path in ingame.mousepressed: calculatePathFromPlayer, then either bufferInput or playerInput for a one-step path. Interruptions and threatened- player truncation therefore behave the way a mouse click does. Most things worth tracking stand on impassable tiles, which calculatePathFromPlayer refuses outright, so we fall back to the cheapest of the 8 neighbours and say "next to" rather than "to". Guarded on states.getCurrent() == "ingame": elsewhere the input buffer belongs to another state and feeding it moves would be a bug. Wall tones now follow the usual proximity convention -- a near wall is loud, one out at radius is quiet. It was inverted, which read backwards in play since a nearby wall is the more urgent fact. config.louderWhenFurther still flips it. Testbed covers both: selection identity across a forced rescan, and that Ctrl+Home moves the player without breaking out of the map. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6380998376
commit
81a209b600
@@ -73,6 +73,7 @@ Finds everything you have seen, sorted by distance.
|
|||||||
| `PageUp` / `PageDown` | Previous / next **kind** of thing in that category |
|
| `PageUp` / `PageDown` | Previous / next **kind** of thing in that category |
|
||||||
| `Shift+PageUp` / `Shift+PageDown` | Previous / next **instance** of that kind |
|
| `Shift+PageUp` / `Shift+PageDown` | Previous / next **instance** of that kind |
|
||||||
| `Home` | Distance, direction, and number of steps by path |
|
| `Home` | Distance, direction, and number of steps by path |
|
||||||
|
| `Ctrl+Home` | Walk to it |
|
||||||
|
|
||||||
Categories: Creatures, Items, Alchemy, Interactables, Exits, Terrain features.
|
Categories: Creatures, Items, Alchemy, Interactables, Exits, Terrain features.
|
||||||
|
|
||||||
@@ -81,6 +82,18 @@ So `Ctrl+PageDown` to reach "Creatures", `PageDown` to move between rat / beetle
|
|||||||
you the walking distance, which indoors is often very different from the
|
you the walking distance, which indoors is often very different from the
|
||||||
straight-line distance.
|
straight-line distance.
|
||||||
|
|
||||||
|
`Ctrl+Home` does exactly what left-clicking the tile does with a mouse: it queues
|
||||||
|
a walk along the game's own path. If the thing itself cannot be stood on — a
|
||||||
|
creature, a chest, a bed — you walk to the nearest tile beside it instead, and
|
||||||
|
the announcement says "next to" rather than "to". As with a mouse click, the walk
|
||||||
|
stops early if something interrupts you, and while an enemy is in view the game
|
||||||
|
only ever commits one step at a time.
|
||||||
|
|
||||||
|
Your selection sticks. The list is rebuilt and re-sorted by distance every turn,
|
||||||
|
but the tracker follows the thing you picked rather than the slot it was in, so a
|
||||||
|
rat you are tracking stays selected as it moves around. It is only let go when it
|
||||||
|
genuinely stops being visible.
|
||||||
|
|
||||||
The tracker only ever reports things you have actually seen.
|
The tracker only ever reports things you have actually seen.
|
||||||
|
|
||||||
### Your character
|
### Your character
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ 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 |
|
||||||
|
| `Ctrl+Home` | Tracker: walk to current instance (the left-click path) |
|
||||||
| `'` | Health and active effects |
|
| `'` | Health and active effects |
|
||||||
| `,` | Current selected action |
|
| `,` | Current selected action |
|
||||||
|
|
||||||
@@ -178,9 +179,9 @@ even while the camera is panned away.
|
|||||||
Step detection **polls** `playerActor.x/y` each frame instead of hooking movement
|
Step detection **polls** `playerActor.x/y` each frame instead of hooking movement
|
||||||
— free, and covers walking, pathing, portals and pushes by construction.
|
— free, and covers walking, pathing, portals and pushes by construction.
|
||||||
|
|
||||||
⚠ `config.louderWhenFurther = true` as requested, but this inverts the usual
|
Volume follows the usual proximity convention: a near wall is loud
|
||||||
convention (proximity cues normally get louder as you approach, and a near wall is
|
(`nearVolume`), one out at `radius` is quiet (`farVolume`). Set
|
||||||
the more urgent fact). Flip the flag in `walltones.lua` if it reads backwards.
|
`config.louderWhenFurther = true` in `walltones.lua` to invert it.
|
||||||
|
|
||||||
Respects `FoV`/`rememberedFoV` — never leaks unseen map geometry.
|
Respects `FoV`/`rememberedFoV` — never leaks unseen map geometry.
|
||||||
|
|
||||||
@@ -319,6 +320,32 @@ remembered. The tracker never reveals what a sighted player couldn't see.
|
|||||||
misleading indoors: a chest 3 tiles north through a wall can be 20 steps away.
|
misleading indoors: a chest 3 tiles north through a wall can be 20 steps away.
|
||||||
Uses the game's own `calculatePathFromPlayer`.
|
Uses the game's own `calculatePathFromPlayer`.
|
||||||
|
|
||||||
|
**`Ctrl+Home` walks there.** The game does support click-to-path: `ingame.mousepressed`
|
||||||
|
(`states/ingame.lua:661`) builds `calculatePathFromPlayer(cursorX, cursorY)` and
|
||||||
|
feeds it to `level.bufferInput("move", path, ingame)`, or to `playerInput` directly
|
||||||
|
when the path is one step. `tracker.walkToSelection` reuses that exact shape with
|
||||||
|
the tracker's selected tile in place of the cursor, so interruptions, threatened-
|
||||||
|
player truncation and animation skipping all behave the way a mouse click does.
|
||||||
|
|
||||||
|
Two wrinkles, both handled:
|
||||||
|
|
||||||
|
* `calculatePathFromPlayer` returns nil for an impassable target, and most things
|
||||||
|
worth tracking (creatures, chests, beds) are impassable. We fall back to the
|
||||||
|
cheapest of the 8 neighbouring tiles and say "walking next to X".
|
||||||
|
* Guard on `states.getCurrent() == "ingame"`. In a menu or observe mode the input
|
||||||
|
buffer belongs to a different state, and handing it moves would be a bug.
|
||||||
|
|
||||||
|
Note `require("states/ingame")` with a slash — that is the module key `states.lua`
|
||||||
|
uses, and requiring `states.ingame` would load a *second* copy of the state.
|
||||||
|
|
||||||
|
**Selection is sticky across rescans.** The scan is rebuilt every turn and both
|
||||||
|
lists are sorted by distance, so holding a bare index means one step by the player
|
||||||
|
silently re-points the selection at a different rat. The tracker instead remembers
|
||||||
|
identity — the actor table for creatures, coordinates for tiles — and re-derives
|
||||||
|
the indices in `reanchor()` after each rescan. Selection is dropped only when the
|
||||||
|
thing stops being reported at all (out of FoV, killed, picked up); the existing
|
||||||
|
clamp then falls back to the nearest.
|
||||||
|
|
||||||
#### ⚠ Correction to D4 — `tryInteract` is NOT a clean "worth tracking" signal
|
#### ⚠ Correction to D4 — `tryInteract` is NOT a clean "worth tracking" signal
|
||||||
`wall` carries a **flavour-only** `tryInteractFunction` ("You touch the wall.
|
`wall` carries a **flavour-only** `tryInteractFunction` ("You touch the wall.
|
||||||
It's cold and rough."), so flag-first classification promoted every wall segment
|
It's cold and rough."), so flag-first classification promoted every wall segment
|
||||||
|
|||||||
@@ -404,6 +404,37 @@ local function scenario()
|
|||||||
press("pagedown", "tracker: next instance", {shift = true})
|
press("pagedown", "tracker: next instance", {shift = true})
|
||||||
press("home", "tracker: announce location")
|
press("home", "tracker: announce location")
|
||||||
|
|
||||||
|
-- Selection stickiness: the scan is rebuilt from scratch and re-sorted by
|
||||||
|
-- distance every turn, so re-deriving the selection by index would silently
|
||||||
|
-- land on a different object. Force a rescan and check we are still on the
|
||||||
|
-- same one.
|
||||||
|
local beforeSelection = tracker.currentSelection()
|
||||||
|
if beforeSelection then
|
||||||
|
tracker.rescan(true)
|
||||||
|
local afterSelection = tracker.currentSelection()
|
||||||
|
check("tracker keeps its selection across a rescan",
|
||||||
|
afterSelection ~= nil
|
||||||
|
and afterSelection.kind == beforeSelection.kind
|
||||||
|
and afterSelection.x == beforeSelection.x
|
||||||
|
and afterSelection.y == beforeSelection.y,
|
||||||
|
beforeSelection.kind .. " at " .. beforeSelection.x .. "," .. beforeSelection.y)
|
||||||
|
|
||||||
|
-- Ctrl+Home walks to the selection, the way a left click does.
|
||||||
|
local _, trackerWorld = pcall(require, "models.world")
|
||||||
|
local level = trackerWorld and trackerWorld.currentLevel
|
||||||
|
local walkFrom = level and level.playerActor
|
||||||
|
local fromX, fromY = walkFrom and walkFrom.x, walkFrom and walkFrom.y
|
||||||
|
press("home", "tracker: walk to selection", {ctrl = true})
|
||||||
|
wait(20)
|
||||||
|
check("ctrl+home did not break out of the map", currentState() == "ingame",
|
||||||
|
"state is " .. tostring(currentState()))
|
||||||
|
if fromX then
|
||||||
|
local now = level.playerActor
|
||||||
|
print(string.format("[testbed] ctrl+home moved player %d,%d -> %d,%d",
|
||||||
|
fromX, fromY, now.x, now.y))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Phase 4: the menu screens reachable from gameplay.
|
-- Phase 4: the menu screens reachable from gameplay.
|
||||||
--
|
--
|
||||||
-- Each is opened, navigated once, and closed. `back` is bound to x/escape/
|
-- Each is opened, navigated once, and closed. `back` is bound to x/escape/
|
||||||
@@ -428,6 +459,7 @@ local function scenario()
|
|||||||
end
|
end
|
||||||
check(screen.state .. " describes focus", focusText() ~= nil,
|
check(screen.state .. " describes focus", focusText() ~= nil,
|
||||||
"focus=" .. tostring(focusText()))
|
"focus=" .. tostring(focusText()))
|
||||||
|
|
||||||
press("escape", "close " .. screen.state)
|
press("escape", "close " .. screen.state)
|
||||||
wait(15)
|
wait(15)
|
||||||
end
|
end
|
||||||
|
|||||||
+208
-4
@@ -6,6 +6,7 @@
|
|||||||
-- PageUp / PageDown kind "rat"
|
-- PageUp / PageDown kind "rat"
|
||||||
-- Shift+PageUp / Shift+PageDown instance that specific rat
|
-- Shift+PageUp / Shift+PageDown instance that specific rat
|
||||||
-- Home announce distance + direction
|
-- Home announce distance + direction
|
||||||
|
-- Ctrl+Home walk there (what a left click does)
|
||||||
--
|
--
|
||||||
-- Two hard rules:
|
-- Two hard rules:
|
||||||
--
|
--
|
||||||
@@ -28,9 +29,15 @@ local categoryIndex = 1
|
|||||||
local kindIndex = 1
|
local kindIndex = 1
|
||||||
local instanceIndex = 1
|
local instanceIndex = 1
|
||||||
|
|
||||||
local scan -- { [categoryKey] = { kinds = { {key, name, instances={{x,y},..}}, .. } } }
|
local scan -- { [categoryKey] = { kinds = { {key, name, instances={{x,y,ref},..}}, .. } } }
|
||||||
local scanSignature
|
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
|
-- Scanning
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
@@ -66,7 +73,9 @@ local function newBucket()
|
|||||||
return {kinds = {}, byKind = {}}
|
return {kinds = {}, byKind = {}}
|
||||||
end
|
end
|
||||||
|
|
||||||
local function addTo(bucket, kindKey, kindName, x, y)
|
-- 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
|
if not kindKey then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -79,7 +88,7 @@ local function addTo(bucket, kindKey, kindName, x, y)
|
|||||||
bucket.kinds[#bucket.kinds + 1] = kind
|
bucket.kinds[#bucket.kinds + 1] = kind
|
||||||
end
|
end
|
||||||
|
|
||||||
kind.instances[#kind.instances + 1] = {x = x, y = y}
|
kind.instances[#kind.instances + 1] = {x = x, y = y, ref = ref}
|
||||||
end
|
end
|
||||||
|
|
||||||
function tracker.rescan(force)
|
function tracker.rescan(force)
|
||||||
@@ -110,7 +119,7 @@ function tracker.rescan(force)
|
|||||||
local visible = level.FoV and level.FoV[actor.x] and level.FoV[actor.x][actor.y]
|
local visible = level.FoV and level.FoV[actor.x] and level.FoV[actor.x][actor.y]
|
||||||
if visible then
|
if visible then
|
||||||
addTo(buckets.creatures, categories.actorKind(actor),
|
addTo(buckets.creatures, categories.actorKind(actor),
|
||||||
categories.actorName(actor), actor.x, actor.y)
|
categories.actorName(actor), actor.x, actor.y, actor)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -166,9 +175,85 @@ function tracker.rescan(force)
|
|||||||
|
|
||||||
scan = buckets
|
scan = buckets
|
||||||
scanSignature = signature
|
scanSignature = signature
|
||||||
|
tracker.reanchor()
|
||||||
return scan
|
return scan
|
||||||
end
|
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
|
-- Access
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
@@ -204,6 +289,23 @@ local function currentInstance()
|
|||||||
return kind.instances[instanceIndex], kind
|
return kind.instances[instanceIndex], kind
|
||||||
end
|
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
|
-- Announcements
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
@@ -284,6 +386,7 @@ function tracker.cycleCategory(delta)
|
|||||||
categoryIndex = cycle(categoryIndex, delta, #categories.list)
|
categoryIndex = cycle(categoryIndex, delta, #categories.list)
|
||||||
kindIndex = 1
|
kindIndex = 1
|
||||||
instanceIndex = 1
|
instanceIndex = 1
|
||||||
|
remember()
|
||||||
announceCategory()
|
announceCategory()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -295,6 +398,7 @@ function tracker.cycleKind(delta)
|
|||||||
end
|
end
|
||||||
kindIndex = cycle(kindIndex, delta, #bucket.kinds)
|
kindIndex = cycle(kindIndex, delta, #bucket.kinds)
|
||||||
instanceIndex = 1
|
instanceIndex = 1
|
||||||
|
remember()
|
||||||
announceKind()
|
announceKind()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -305,6 +409,7 @@ function tracker.cycleInstance(delta)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
instanceIndex = cycle(instanceIndex, delta, #kind.instances)
|
instanceIndex = cycle(instanceIndex, delta, #kind.instances)
|
||||||
|
remember()
|
||||||
announceInstance()
|
announceInstance()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -338,6 +443,104 @@ function tracker.announceLocation()
|
|||||||
speech.say(table.concat(parts, ", "), true)
|
speech.say(table.concat(parts, ", "), true)
|
||||||
end
|
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
|
-- Key registration
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
@@ -353,6 +556,7 @@ function tracker.registerKeys(hotkeys)
|
|||||||
hotkeys.bind("pagedown", nil, function() tracker.cycleKind(1) end)
|
hotkeys.bind("pagedown", nil, function() tracker.cycleKind(1) end)
|
||||||
|
|
||||||
hotkeys.bind("home", nil, tracker.announceLocation)
|
hotkeys.bind("home", nil, tracker.announceLocation)
|
||||||
|
hotkeys.bind("home", "ctrl", tracker.walkToSelection)
|
||||||
end
|
end
|
||||||
|
|
||||||
return tracker
|
return tracker
|
||||||
|
|||||||
+10
-14
@@ -15,9 +15,9 @@
|
|||||||
--
|
--
|
||||||
-- i.e. 0.4 world units per tile, listener floating 4 units above the map plane.
|
-- i.e. 0.4 world units per tile, listener floating 4 units above the map plane.
|
||||||
--
|
--
|
||||||
-- If we simply placed each tone at its wall's real position, OpenAL would
|
-- If we simply placed each tone at its wall's real position, OpenAL's own
|
||||||
-- attenuate it by distance -- so a far wall would be QUIETER. That directly
|
-- attenuation curve would decide the volume for us -- unpredictable, and tied to
|
||||||
-- fights the requested behaviour (far wall = louder). It would also be feeble
|
-- whatever distance model the game happens to set. It would also be feeble
|
||||||
-- directionally: with the listener 4 units up and a wall 5 tiles out (2.0 units),
|
-- directionally: with the listener 4 units up and a wall 5 tiles out (2.0 units),
|
||||||
-- the vertical offset dominates and the pan collapses toward centre.
|
-- the vertical offset dominates and the pan collapses toward centre.
|
||||||
--
|
--
|
||||||
@@ -46,17 +46,12 @@ local config = {
|
|||||||
-- constant attenuation, not the perceived direction.
|
-- constant attenuation, not the perceived direction.
|
||||||
sourceRadius = 1.0,
|
sourceRadius = 1.0,
|
||||||
|
|
||||||
-- Volume at the near and far ends of the range.
|
-- Volume for a wall you are standing next to, and for one out at `radius`.
|
||||||
nearVolume = 0.25,
|
nearVolume = 1.0,
|
||||||
farVolume = 1.0,
|
farVolume = 0.25,
|
||||||
|
|
||||||
-- As requested: a wall further away is LOUDER.
|
-- Set true to invert: a wall further away becomes the louder one.
|
||||||
--
|
louderWhenFurther = false,
|
||||||
-- Flagging this because it inverts the usual convention -- most proximity cues
|
|
||||||
-- get louder as you approach, and a nearby wall is the more urgent fact. If it
|
|
||||||
-- reads backwards in play, set this to false and near walls become the loud
|
|
||||||
-- ones; nothing else needs to change.
|
|
||||||
louderWhenFurther = true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
walltones.config = config
|
walltones.config = config
|
||||||
@@ -134,10 +129,11 @@ local function volumeFor(distance)
|
|||||||
local t = (distance - 1) / math.max(1, config.radius - 1)
|
local t = (distance - 1) / math.max(1, config.radius - 1)
|
||||||
t = math.max(0, math.min(1, t))
|
t = math.max(0, math.min(1, t))
|
||||||
|
|
||||||
if not config.louderWhenFurther then
|
if config.louderWhenFurther then
|
||||||
t = 1 - t
|
t = 1 - t
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- t is now 0 at the loud end, 1 at the quiet end.
|
||||||
return config.nearVolume + (config.farVolume - config.nearVolume) * t
|
return config.nearVolume + (config.farVolume - config.nearVolume) * t
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user