docs: final handoff pass

Adds accessibility/README.md, the player-facing setup and keymap reference --
the thing to read before playtesting.

Brings the developer docs in line with what was actually built:

- docs/README.md rewritten as an index with current status and a one-paragraph
  architecture summary.
- docs/scratch.md gains a HANDOFF section: state of play, the commands to run
  after any change, the five traps that each cost real debugging time, what is
  NOT verified and why, deliberate omissions, and how to rebase onto a future
  game version.
- docs/03-implementation-plan.md gains a status banner listing where the
  implementation deliberately diverged from the plan (menu narration pulled
  forward as a blocker, levelCursorPopup not patched, tryInteract insufficient
  as a tracker filter, testbed added, no settings UI built).
- docs/04-open-questions.md gains a resolution banner: Q6 and Q8 remain open.

Also records that the testbed check count varies between runs (23-26) because
some blocks are conditional, so what matters is zero failures rather than the
total.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Accessibility Mod
2026-08-01 22:25:09 +02:00
co-authored by Claude Opus 5
parent 96ec79ed75
commit 6380998376
5 changed files with 284 additions and 15 deletions
@@ -1,5 +1,28 @@
# Implementation plan
> ## ⚠ Status: Phases 0–5 are COMPLETE.
>
> This document is the *original* plan, kept for its reasoning. Where the
> implementation diverged, the reason is recorded in
> [scratch.md](scratch.md) and [05-decisions.md](05-decisions.md).
>
> Notable divergences from what is written below:
>
> * **Phase 4 was pulled forward, partly into Phase 1's slot.** Menu narration
> turned out to be a hard blocker — the screen name was spoken but not the
> selected option, so a blind player could not start a new game at all.
> * **`levelCursorPopup` is NOT patched** (contrary to Phase 2a below). It fires
> from an animation-completion callback and lands a keypress late; the
> `ingame_observe` descriptor polls the cursor position instead.
> * **`tile.tryInteract` is not a sufficient tracker filter** (contrary to
> Phase 3 below). `wall` carries a flavour-only `tryInteractFunction`.
> See the D4 correction in `05-decisions.md`.
> * **A code-driven testbed was added** (`accessibility/testbed.lua`), not in the
> original plan. It found five real bugs that would otherwise have surfaced
> only during play.
> * **Phase 5's mod-settings screen was not built.** Settings exist as fields in
> the relevant modules and are not yet exposed at runtime.
Phases are ordered so each one is independently playable-and-testable. Nothing later is required for something earlier to be useful.
---
+14
View File
@@ -1,5 +1,19 @@
# Open questions
> ## ⚠ Status: mostly answered — see [05-decisions.md](05-decisions.md).
>
> **Answered:** Q1 (dev LÖVE; DLLs load by absolute path from
> `accessibility/lib/`, no elevation needed) · Q2 (tracker hierarchy confirmed) ·
> Q3 (augment, not relay) · Q4 (speak everything queued; an explicit game action
> cancels the backlog) · Q5 (wall tones on step, stop-and-replay — confirmed by
> ear) · Q7 + Q10 (small patches allowed, probe backend kept open for a Steam
> release) · Q9 (NVDA; `lovec` approved, and a code-driven testbed was built).
>
> **Still open:** **Q6** — our hotkeys are currently a separate scheme and are
> therefore not user-rebindable; integrating them into the game's own rebinding
> UI is still undecided. **Q8** — no exhaustive keyboard-reachability audit was
> performed.
Ordered roughly by how much they block work. Q1–Q3 affect the design; the rest are preferences I can pick defaults for if you'd rather not decide up front.
---
+42 -13
View File
@@ -1,23 +1,52 @@
# Accessibility mod — documentation
# Accessibility mod — developer documentation
Research and planning for making *Sulphur Memories: Alchemist* 0.4.2 blind-accessible.
Making *Sulphur Memories: Alchemist* 0.4.2 blind-accessible.
Player-facing keymap and setup: `accessibility/README.md`.
**Picking this up mid-stream? Read [scratch.md](scratch.md) first.** It is the
living handoff document: current state, every bug found and why, and the traps.
| Doc | Contents |
|---|---|
| [01-architecture.md](01-architecture.md) | How the game is built: states, console, level model, keybindings, where text already exists. |
| [02-hooking-strategy.md](02-hooking-strategy.md) | How to attach the mod with (near) zero edits to game files. |
| [03-implementation-plan.md](03-implementation-plan.md) | Six phases, each independently testable. |
| [04-open-questions.md](04-open-questions.md) | Decisions needed before/while building. |
| [05-decisions.md](05-decisions.md) | Answers given so far, and what they imply. Supersedes Q1/Q2/Q7/Q10. |
| [scratch.md](scratch.md) | **Start here.** Working state, bugs found, gotchas, keymap, how to run the testbed. |
| [01-architecture.md](01-architecture.md) | How the game is built: states, console, level model, keybindings. |
| [02-hooking-strategy.md](02-hooking-strategy.md) | How the mod attaches with (near) zero edits to game files. |
| [03-implementation-plan.md](03-implementation-plan.md) | The original six-phase plan, with outcomes. |
| [04-open-questions.md](04-open-questions.md) | Questions raised; see 05 for answers. |
| [05-decisions.md](05-decisions.md) | Decisions made, and the D4 correction. |
## The short version
## Status
Three findings shape everything:
**Phases 0–5 complete.** 23 state descriptors, 5 patches, 26/26 testbed checks,
history unit tests passing.
1. **The game is already keyboard-complete.** All 28 states implement `keypressed`; there's a full rebindable-keys system with contexts. The accessibility gap is that *nothing announces what is focused* — not that things are unreachable. This makes the job much smaller than a typical mouse-driven game.
**Game-file footprint: one line in `main.lua`.** Verify at any time:
2. **The game already writes the text we need to speak.** `console.print` is a single chokepoint for all log messages. `level.getCursorMessage()` returns tile/creature descriptions. `getCursorPopupStats()` feeds the item popups in five screens. We are mostly *re-routing existing strings to speech*, not writing a description engine.
```
git diff --stat vanilla-0.4.2 -- . ':!accessibility' ':!.gitignore'
```
3. **Everything is a `require`d singleton, and `debug.getupvalue` works.** Module functions can be monkey-patched in place, and the file-local `selectedOption` variables in menu states are readable from outside. A one-line bootstrap in `main.lua` is the only unavoidable edit.
## The three findings that shaped everything
Proposed footprint: **1–3 lines added to `main.lua`**, everything else new files under `accessibility/`. A git baseline tagged `vanilla-0.4.2` makes re-application on future versions a rebase.
1. **The game was already keyboard-complete.** All 28 states implement
`keypressed`, with a full rebindable-keys system. The gap was that nothing
*announced* what was focused — so the job was narration, not reimplementation.
2. **The game already writes the text.** `console.print` is a single chokepoint
for all log messages; `level.getCursorMessage()` describes tiles;
`inventoryCursorPopup.set` covers four screens. Mostly we re-route existing
strings to speech rather than writing a description engine.
3. **Everything is a `require`d singleton, and `debug.getupvalue` works.** Module
functions can be patched in place, and file-local focus variables
(`selectedOption`, …) are readable from outside. Hence the one-line footprint.
## Architecture in one breath
`main.lua` calls `require("accessibility").initialize()`. That patches module
singletons (`console.print`, `popupStack.add`, `inventoryCursorPopup.set`,
`modules.slider`), wraps `states.switch` / `update` / `keypressed` / `textinput`,
and installs a global hotkey layer. Per-screen narration lives in
`accessibility/states/<statename>.lua` descriptors, read via `debug.getupvalue`.
A startup self-test reports which patches and probes still resolve — the canary
for a future game version moving something.
+86 -2
View File
@@ -2,8 +2,8 @@
**Read this first on any handoff.** Living document: update it whenever state changes, before ending a turn.
Last updated: 2026-08-01 · Currently: **Phases 0–3 done + menus/dialogue.
Phases 4-5 done. Remaining: polish, mod settings, distribution.**
Last updated: 2026-08-01 · Currently: **Phases 0–5 COMPLETE. Handed off for
playtesting.** 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 —
@@ -18,6 +18,11 @@ rm -rf ~/AppData/Roaming/LOVE/Alchemist # start from a clean save
It drives the game by code via `love.event.push`, navigating by *reading* focus
rather than counting keypresses. Every bug listed below was caught by it.
⚠ **The check COUNT varies between runs (23–26); this is expected.** Several
blocks are conditional — the config-menu section only runs if `selectByText`
lands on "Options", and mouse-hover can move menu focus at startup (see trap 2).
**What matters is `0 failures`, not the total.**
---
## What this project is
@@ -489,3 +494,82 @@ utterance — speech is verifiable from the log, not only by ear. `!` after
- `.luacheckrc` uses `std = "min+alch"` with an explicit globals list — add mod globals there to keep lint clean.
- Upstream quirk (not ours to fix): `modules/tile/preprocess.lua` flattens parents in a single `pairs()` pass, so a grandchild may inherit from a not-yet-flattened parent depending on hash order.
- Tolk DLL sources on this machine: `D:\code\cafe`, `D:\code\dragonrpg-mapmaker`, `D:\code\DragonWarriorAccess`.
---
## HANDOFF — read this if you are resuming
### State of play
Phases 0–5 complete. **23 state descriptors, 5 patches, 26/26 testbed checks**,
history unit tests passing, **one line changed in the game** (`main.lua`).
Everything in the original brief is implemented: console speech with
line-by-line review, the object tracker with the requested key scheme, tile
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
rm -rf ~/AppData/Roaming/LOVE/Alchemist # clean save
"C:/Program Files/LOVE/lovec.exe" . --a11y-test # ~4 min, 26 checks
git diff --stat vanilla-0.4.2 -- . ':!accessibility' ':!.gitignore' # must stay: main.lua | 1 +
```
### The five traps (all cost real debugging time — don't rediscover them)
1. **`love.keypressed` is captured by value.** `states.setup()` runs
`love[cb] = love[cb] or states[cb]` *before* us. Wrapping the `states` table
field alone does nothing for `keypressed`/`textinput` — you must also
reassign `love.keypressed`. `switch`/`update` are fine (looked up at call
time). **Any new callback you wrap needs the same treatment.**
2. **Focus moves without a keypress.** `main_menu.mousemoved` assigns
`selectedOption` from cursor position. Hence focus is *polled* in the update
hook, not announced only on keypress.
3. **Nested `states.switch` announces inner-first.** `ingame.open()` can switch
straight to `ingame_dialogue`. We announce after `realSwitch` returns, so the
outer announcement would clobber the inner one — guarded by checking
`states.getCurrent() ~= newState`.
4. **`states.lua` requires with a SLASH** (`require("states/"..name)`).
`require("states.main_menu")` loads a *second independent copy* with its own
upvalues. Always match the slash.
5. **Probes may need chaining or a cross-function search.** Some locals are
upvalues of an inner function (`narration` → `continueNarration`) or are only
touched in `open()` (`actions_menu`, `worldmap`). Use `menus.getAnyUpvalue`.
Use `menus.hasUpvalue` when you need "exists" rather than "has a value".
### Not verified — needs a real playthrough
These are blocked on content the prologue does not contain, **not** known broken:
- **Tracker categories** Items / Alchemy / Exits / Creatures / Terrain. Only
Interactables is populated in the tutorial house. Diagnostic: the testbed
prints `tracker contents:` with per-category kind names.
- **Inventory item names** — inventory is empty in the prologue, so only the
"empty slot" path has run.
- **The quantity slider** (`patches/slider.lua`). Confirmed *applied* by the
self-test but never exercised: `ingame_wait` is unreachable because
`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.
- **Popup announcements** (`patches/popupstack.lua`) — loot/quest/recipe popups
did not fire during the prologue run.
### 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.
- **Q6 unanswered**: our hotkeys are a separate scheme, not registered in the
game's own rebinding UI. They are therefore not user-rebindable.
- **Q8 unanswered**: no exhaustive keyboard-reachability audit was done. All 28
states implement `keypressed`, and every screen reached so far was navigable,
but this was not proven screen-by-screen.
- **Distribution not designed** (D1 deferred). Any shipped build must place the
Tolk DLLs beside the executable — they cannot live inside a `.love`.
### If a future game version breaks something
Run the game and read the startup self-test. It reports every patch and every
descriptor probe. `PROBE BROKEN: no upvalue 'x'` means that local was renamed —
fix the descriptor in `accessibility/states/<state>.lua`. `ok (unset until
opened)` is normal.
To rebase the mod onto a new game version: commit the new pristine tree on top
of the `vanilla-0.4.2` baseline, then replay the mod commits. The only game-file
conflict possible is the single `main.lua` line.