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:
co-authored by
Claude Opus 5
parent
96ec79ed75
commit
6380998376
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user