orientation: the travel map, unexplored ways, the nearest exit
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ce0f375a10
commit
5778e86e8e
@@ -7,7 +7,7 @@ fixing what Talon hits. Remaining work is polish, mod settings and distribution.
|
||||
|
||||
**Game is playable start-to-finish by ear** from main menu through the intro
|
||||
conversation into gameplay, plus journal, character, actions and inventory —
|
||||
verified by the testbed, **98/98 checks**.
|
||||
verified by the testbed, **120 checks, 0 failures**.
|
||||
|
||||
**Not set up yet, or unsure how the two trees relate?** →
|
||||
[00-dev-setup.md](00-dev-setup.md).
|
||||
@@ -153,14 +153,17 @@ and by Talon at the keyboard (wall tones).
|
||||
| `'` | Health and active effects |
|
||||
| `,` | Current selected action |
|
||||
| `=` / `Shift+=` | Read details of the focused thing (Shift = keep the name line) |
|
||||
| `End` | Frontier report: unexplored ways, nearest first |
|
||||
| `Shift+End` | Nearest known exit (does not touch the tracker selection) |
|
||||
|
||||
Game keys worth knowing: **`x`** enters examine mode (not `e`), `f` finds exits
|
||||
while examining, `j` journal, `i` inventory, `c` character, `a` actions menu.
|
||||
|
||||
Reserved for the tracker (Phase 3): `PageUp`/`PageDown`, `Home`, with Ctrl/Shift.
|
||||
|
||||
**Free scancodes** (game binds none of these): `[ ] \ ; ' , / - =`, `h k l m n o
|
||||
p u v w`, `F1`–`F12`, `pageup pagedown home end insert`, `kp0 kp. kp+ kp- kp* kp/`.
|
||||
**Free scancodes** (game binds none of these): `- `, `h k l m n o p u v w`,
|
||||
`F1`–`F12`, `insert`, `kp0 kp. kp+ kp- kp* kp/`.
|
||||
*Now taken by the mod:* `[ ] \ ; ' , / =` `pageup pagedown home end`.
|
||||
|
||||
**Game-claimed scancodes — never bind these unmodified:**
|
||||
`. 0-9 \` a b backspace c d delete down e escape f g i j kp1-kp9 kpenter lalt
|
||||
@@ -881,6 +884,117 @@ does: `ingame.interface("research")`, the exact call in the desk's
|
||||
module whose `levelWindow` is nil, which `research.draw` indexes on its first
|
||||
frame.
|
||||
|
||||
### Done — orientation: world map, frontier report, nearest exit (Talon request)
|
||||
|
||||
Prompted by "do sighted players have a minimap we don't have access to?" The
|
||||
answer is no widget, but two map *surfaces* were only half exposed.
|
||||
|
||||
**Two corrections worth not rediscovering:**
|
||||
|
||||
- **`level.exits` does not exist at runtime.** It is packed as nil
|
||||
(`modules/level.lua:416`) and never assigned anywhere; `exits` is a *generator*
|
||||
parameter baked into individual tiles. The primitive to use is the game's own
|
||||
**`level.getListByTag("exit")`** (`modules/level.lua:1382`) — it walks the grid,
|
||||
gates on `FoV or rememberedFoV`, skips hidden tiles, and returns `{x, y}` sorted
|
||||
by distance to the player. The FoV rule is then enforced by vanilla rather than
|
||||
restated by us. The tag set is `way_out`, `building_door`, `horse`, `ladder_up`,
|
||||
`ladder_down` (`modules/tile/templatesByTag.lua` — the only tag the game defines).
|
||||
- **Exits were already a tracker category** (`categories.lua:41`, classified
|
||||
*before* the lootable and structural filters so nothing can steal them). The new
|
||||
key is a shortcut, not a new capability.
|
||||
|
||||
**1. World map (`accessibility/states/worldmap.lua`, rewritten).** Was speaking
|
||||
name / completed / favourite / `visitedMessage` only, dropping the three things
|
||||
the *picture* carries: area type, geography, travel time.
|
||||
|
||||
Switched from `probe`/`render` to `read()` + `details()` — necessarily, because
|
||||
`render(entry, index, total)` gets no state table and the travel time needs the
|
||||
screen's `currentAreaId`. All three locals (`options`, `selectedOption`,
|
||||
`currentAreaId`, `states/worldmap.lua:112-114`) are upvalues of exported
|
||||
functions, so `menus.getAnyUpvalue` reaches them. Travel time is the game's own
|
||||
`worldmapModel.getDistanceMessage`, the same call the screen makes at
|
||||
`states/worldmap.lua:360` — already finished English, including "You are
|
||||
currently here."
|
||||
|
||||
Two gotchas found on the way:
|
||||
|
||||
- `options[i].icon` is already an **image object** (`states/worldmap.lua:240`).
|
||||
The icon *id* string lives on the model area, `worldmapModel.areas[id].icon`.
|
||||
Do **not** add a name table to `models/interface/mapIcons.lua`:
|
||||
`icons.preprocess` iterates it with `pairs` and would treat a new table as an
|
||||
image. All 20 ids are readable once the underscores go, so it is a `gsub`
|
||||
rather than a lookup — new ids in a future version read sensibly instead of
|
||||
vanishing.
|
||||
- `describe.direction` only tests the **sign** of each delta, so on the map's
|
||||
floating-point 0..1 coordinates it called almost everything diagonal. There is
|
||||
now a 25% deadzone, with the same `4/3` x-correction the game's own distance
|
||||
maths uses (`models/worldmap.lua:64`), so the bearing agrees with the travel
|
||||
time.
|
||||
|
||||
**New: `descriptor.probeNames`** (`menus.lua`, selfTest). A `read()` descriptor
|
||||
has no probe table, so it previously reported a bare `custom read()` and lost the
|
||||
version-bump canary entirely — on exactly the screens that reach for the most
|
||||
upvalues. A descriptor can now declare the game locals it depends on and get the
|
||||
same `PROBE BROKEN: no upvalue 'x'` check. `worldmap` and `research` both declare
|
||||
theirs.
|
||||
|
||||
**2. Frontier report, `End` (`accessibility/frontier.lua`, new).** The level view
|
||||
*is* the map: `rememberedFoV` keeps everything ever seen on screen, and a sighted
|
||||
player pans over it and reads the fog edge. Nothing in the mod answered "where
|
||||
haven't I been" — surroundings is local, examine is one tile, the tracker only
|
||||
finds things it already knows about.
|
||||
|
||||
A frontier tile is: seen, **not** `blocksMovement`, with at least one **cardinal**
|
||||
neighbour that is **in bounds** and **not seen**. Both restrictions are
|
||||
load-bearing —
|
||||
|
||||
- *in bounds*, or every walkable tile on the grid boundary is a frontier and the
|
||||
report is nothing but map edge;
|
||||
- *cardinal, not all 8*, or the diagonal test fires on the unseen tile behind
|
||||
every wall corner, which is not anywhere you can walk. Standing in a lit
|
||||
corridor would report an "unexplored way" per corner.
|
||||
|
||||
Clustered 8-connected (so a wide doorway is one way, but two separate openings
|
||||
that both lie north stay two), reduced to each cluster's nearest member, sorted,
|
||||
capped at 4 with "and N more". Flood fill uses an explicit stack, not recursion.
|
||||
|
||||
**Not a cheat:** reads only `FoV`/`rememberedFoV` and reports only *that* an
|
||||
opening exists, never what is beyond it — exactly what the fog edge shows.
|
||||
|
||||
**3. Nearest exit, `Shift+End`** (`tracker.announceNearestExit`). Lives in
|
||||
`tracker.lua` for the level access, `offsetFromPlayer` and the path-length logic,
|
||||
but deliberately **does not touch the category/kind/instance selection** — that is
|
||||
the whole reason it is a separate key rather than a category jump. Uses
|
||||
`getListByTag` rather than the tracker's own scan.
|
||||
|
||||
**4. "remembered" promoted to normal verbosity** (`describe.lua`). It was
|
||||
verbose-only. `tileFacts` reads actors **only when the 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". It is a safety flag, not a nicety.
|
||||
|
||||
**120 testbed checks, 0 failures** (was 98). Two bugs the testbed caught that
|
||||
review had not:
|
||||
|
||||
- The world map said **"You are currently here." twice**. `getDistanceMessage`
|
||||
and `getLastVisitedMessage` return the *same sentence* for the area you are
|
||||
standing in (`models/worldmap.lua:75-77` and `:106-107`). On screen they are two
|
||||
separate panels and nobody notices; by ear it is a stutter. `read()` now drops
|
||||
`visitedMessage` when it duplicates the travel line. **General lesson: two
|
||||
game strings that are fine side by side on screen can collide when concatenated
|
||||
into one utterance.**
|
||||
- The first version of the frontier test walked one step east and demanded the
|
||||
numbers change. It failed on a run where nothing was wrong — the step landed,
|
||||
but both openings kept the same Chebyshev distance. Replaced with the property
|
||||
that actually matters and is deterministic: every reported distance must equal
|
||||
Chebyshev from the player's *current* position, and the list must be sorted
|
||||
nearest first.
|
||||
|
||||
The testbed opens the world map the way a travel tile does —
|
||||
`ingame.interface("worldmap", {locationId = 1})` — and never presses enter, which
|
||||
would travel and take the run out of the prologue. Same `states/ingame` slash-form
|
||||
require trap as the research tree.
|
||||
|
||||
### Known gaps (next up)
|
||||
- **Phase 5**: player status key (HP, statuses, current action), popup
|
||||
announcements (`popupStack.add`), damage alerts
|
||||
@@ -945,7 +1059,7 @@ utterance — speech is verifiable from the log, not only by ear. `!` after
|
||||
## HANDOFF — read this if you are resuming
|
||||
|
||||
### State of play
|
||||
Phases 0–5 complete. **24 state descriptors, 7 patches, 98/98 testbed checks**,
|
||||
Phases 0–5 complete. **24 state descriptors, 7 patches, 120 testbed checks (0 failures)**,
|
||||
history unit tests passing, **one line changed in the game** (`main.lua`).
|
||||
|
||||
Everything in the original brief is implemented: console speech with
|
||||
@@ -955,7 +1069,7 @@ exploration, wall tones, and keyboard+speech access to every screen.
|
||||
### First thing to do after any change
|
||||
```
|
||||
lua accessibility/tests/test_history.lua # pure Lua, instant
|
||||
bash accessibility/tests/run-testbed.sh # ~4 min, 98 checks
|
||||
bash accessibility/tests/run-testbed.sh # ~4 min, 120 checks
|
||||
git diff --stat vanilla-0.4.2 -- . ':!accessibility' ':!.gitignore' ':!CLAUDE.md' # must stay: main.lua | 1 +
|
||||
```
|
||||
|
||||
@@ -1008,6 +1122,24 @@ These are blocked on content the prologue does not contain, **not** known broken
|
||||
recipe / lore / map popups are a separate mechanism and are now covered by the
|
||||
`ingame_popup` descriptor and the testbed.)
|
||||
|
||||
- **The nearest-exit key (`Shift+End`) against a real exit.** The tutorial house
|
||||
has no exit-tagged tile in view, so `getListByTag("exit")` returns an empty list
|
||||
and live it has only ever said `No exits known`. The testbed proves the call
|
||||
still answers and that the key leaves the tracker selection alone; it has not
|
||||
proven the sentence.
|
||||
- **The frontier report (`End`) on a real dungeon.** In the prologue house it
|
||||
finds 2 clusters, both south-west, which is the right shape for one door but is
|
||||
not much of a test of the clustering. The invariants (never an unseen tile,
|
||||
never an impassable one, every cluster really touches unexplored ground,
|
||||
distances measured from the player, nearest first) are all checked, but the
|
||||
*phrasing* wants judging by ear on a level with several branches. In
|
||||
particular: whether connected components read better than bucketing by bearing,
|
||||
and whether a bare direction is enough or each way wants a hint of what it is.
|
||||
- **The world map with more than one area.** The prologue has only Vyshberek, so
|
||||
the bearing clause and a real travel duration have never been spoken — every
|
||||
run so far says "You are currently here." The deadzone in `bearing()` (25%)
|
||||
is therefore untuned by ear.
|
||||
|
||||
### Known-missing features (deliberate, not bugs)
|
||||
- **No mod settings UI.** Verbosity (`describe.verbosity`), wall-tone config
|
||||
(`walltones.config`) and `speech.logToConsole` are code-level fields only.
|
||||
|
||||
Reference in New Issue
Block a user