loot overlay: speak the contents of visible containers

The tutorial teaches "press s to see the contents of containers", and it was
silent. For a sighted player s sets levelWindow.itemsHighlightEnabled, which
draws the top item's icon on every loot-bearing tile in FoV.

We watch that flag rather than binding s ourselves -- it is game-claimed, and
keybindings.getCommand() ignores modifiers -- and speak on its rising edge, so
the player's own binding, hold vs toggle mode and the toolbar icon all work.
The flag is checked both at end of frame and inside the keypressed wrapper,
because in hold mode a quick tap goes up and down within one frame.

Reads the whole tile.loot list, not just the icon's top entry, nearest first,
with each container pushed to the console history for [ ] review.

71 testbed checks, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Accessibility Mod
2026-08-03 01:45:18 +02:00
co-authored by Claude Opus 5
parent dddd8ed9de
commit 93ef26349a
5 changed files with 529 additions and 3 deletions
+62 -3
View File
@@ -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, **59/59 checks**.
verified by the testbed, **71/71 checks**.
**Not set up yet, or unsure how the two trees relate?** →
[00-dev-setup.md](00-dev-setup.md).
@@ -684,6 +684,62 @@ indistinguishable from a dropped key).
prologue inventory is empty and craft/refine/trade need an alchemy table. The
live path is Talon's to confirm.
### Done — the loot overlay key `s` (Talon request)
**Symptom:** the tutorial says *"Press 's' to see the contents of containers"*
and nothing was spoken. That is the one mechanic the game explicitly teaches for
finding reagents, and alchemy starts with finding reagents.
**What `s` actually does.** It is the `lootOverlay` command, and it sets a single
flag — `levelWindow.itemsHighlightEnabled`. `views/levelWindow.lua:818` then
draws, over every tile in **current FoV**, the icon of
`levelModel.getItemOnCoordinates(x,y)` — i.e. the **topmost item of the topmost
loot-bearing tile**. So the sighted affordance is "there is something in that
chest", one icon per tile, not a contents list.
**`accessibility/lootoverlay.lua`** speaks the same set of tiles, nearest first,
and reads out the **whole `tile.loot` list** rather than just the top entry —
the icon can only show one, we are not so limited. Blocks are pushed into the
console history so `[` / `]` / `\` walk a long list back. `No loot in view` when
empty: silence would be indistinguishable from a dropped key.
Live text: `Tiled man-made floor, here, A bunch of ivy leaves, 3`.
Decisions worth not relitigating:
- **No key of ours.** `s` is game-claimed and `keybindings.getCommand()` ignores
modifiers, so binding it in our layer would collide. We watch the *flag*
instead, on its **rising edge**. That covers the key in both hold and toggle
mode (`holdForItemHighlight`, which defaults to **hold**), the toolbar icon,
the player's own rebinding, and all four ingame states — they share one
`levelWindow` instance, created once in `ingame.open` and passed on by params.
- **The flag is checked in TWO places.** The end-of-frame poll is not enough on
its own: in hold mode a quick tap pushes keypressed and keyreleased inside one
frame, so the poll only ever sees it back down. `init.lua`'s `states.keypressed`
wrapper therefore also calls `lootoverlay.update()` *between* the game's
keypressed and its keyreleased. The poll still earns its place — it is what
catches the mouse and the key being released.
- **`levelWindow` is reached by `menus.getAnyUpvalue` on `states/ingame`** and
cached (it is created once, not per level). Covered by the startup self-test
(`[accessibility] loot overlay: ok`) as the version-bump canary.
- **Loot entries are item *params*, not items** — `{templateId, quantity}`;
`modules/lootPopup.lua` only builds real items when the popup opens. Names come
from `modules/item/templates`, so nothing is allocated to say a name.
- **Membership matches levelWindow's test exactly**: current `FoV` only, not
hidden, `visibilityAlpha ~= 0`, non-empty loot. `conditionalLoot` is *not*
read — the overlay does not draw it either. Invariant 4 holds: nothing is said
that is not being drawn on screen at that moment.
- `unlootable` containers are still reported (the overlay draws them) but tagged
`cannot be taken`. Scan is bounded to a 20-tile box and runs on the edge only,
never per frame — invariant 5.
**71 testbed checks, 0 failures** (was 59). The prologue has no loot at all
(`chest_tutorial` is empty by design), so the live path is exercised by
**planting `ivy_leaves` on the tile under the player, scanning, and putting the
tile back** — otherwise only the "nothing here" branch would ever run. Plus the
pure phrasing checks, the FoV invariant over the real grid, the `levelWindow`
probe, and an end-to-end `s` press.
### Known gaps (next up)
- **Phase 5**: player status key (HP, statuses, current action), popup
announcements (`popupStack.add`), damage alerts
@@ -748,7 +804,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. **23 state descriptors, 6 patches, 59/59 testbed checks**,
Phases 0–5 complete. **23 state descriptors, 6 patches, 71/71 testbed checks**,
history unit tests passing, **one line changed in the game** (`main.lua`).
Everything in the original brief is implemented: console speech with
@@ -758,7 +814,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, 59 checks
bash accessibility/tests/run-testbed.sh # ~4 min, 71 checks
git diff --stat vanilla-0.4.2 -- . ':!accessibility' ':!.gitignore' ':!CLAUDE.md' # must stay: main.lua | 1 +
```
@@ -803,6 +859,9 @@ These are blocked on content the prologue does not contain, **not** known broken
`tutorial_bed` has an empty `tryInteractFunction` by design, and
craft/refine/trade need an alchemy table and real items. The testbed already
has `walkTo()` and tracker-based bed lookup ready for when one is reachable.
- **The loot overlay (`s`) against real loot.** The prologue contains none, so
live it has only ever said `No loot in view`. The contents path is proven by
the testbed's planted-loot check, not by a real chest.
- **Popup announcements** (`patches/popupstack.lua`) — loot/quest/recipe popups
did not fire during the prologue run.