diff --git a/CLAUDE.md b/CLAUDE.md index 805b05f..a45b46e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -65,10 +65,14 @@ worth knowing before you write a line. ``` 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 +bash accessibility/tests/run-testbed.sh # ~4 min ``` +**Always use that script — never call `lovec . --a11y-test` directly, and never +`rm -rf` the save directory.** The testbed needs an empty save to drive a fresh +playthrough; the script parks the player's real save, runs, and puts it back. +Deleting the save by hand destroys a real playthrough. + The testbed drives the real game by code and navigates by *reading focus* rather than counting keypresses. It has caught essentially every bug in this project, so run it — a change that looks obviously correct is exactly the kind that fails. diff --git a/accessibility/.gitattributes b/accessibility/.gitattributes new file mode 100644 index 0000000..85df959 --- /dev/null +++ b/accessibility/.gitattributes @@ -0,0 +1,7 @@ +# Per-directory .gitattributes, kept inside accessibility/ deliberately: a +# top-level one would show up in the game-file footprint check +# (git diff --stat vanilla-0.4.2 ...), which must stay at `main.lua | 1 +`. +# +# Shell scripts must keep LF endings even on Windows checkouts: bash fails with +# "\r: command not found" on CRLF, which would break tests/run-testbed.sh. +*.sh text eol=lf diff --git a/accessibility/docs/00-dev-setup.md b/accessibility/docs/00-dev-setup.md index 720923d..4d69fa8 100644 --- a/accessibility/docs/00-dev-setup.md +++ b/accessibility/docs/00-dev-setup.md @@ -122,8 +122,7 @@ cd "D:/code/Sulphur Memories Alchemist 0.4.2 sanctified" lua accessibility/tests/test_history.lua # pure Lua, instant luac -p accessibility/*.lua # syntax only, no LOVE needed -rm -rf ~/AppData/Roaming/LOVE/Alchemist # clean save -"C:/Program Files/LOVE/lovec.exe" . --a11y-test # ~4 min +bash accessibility/tests/run-testbed.sh # ~4 min — save-safe, see below git diff --stat vanilla-0.4.2 -- . ':!accessibility' ':!.gitignore' ':!CLAUDE.md' ``` @@ -143,8 +142,29 @@ To just play it: ### Save data LÖVE writes to `~/AppData/Roaming/LOVE/Alchemist` (`t.identity = "Alchemist"` in -`conf.lua`). Deleting it is how you get a clean prologue; the testbed scenario -assumes a fresh save and will not navigate correctly without one. +`conf.lua`). The testbed scenario assumes a fresh save and will not navigate +correctly without one — it drives a new game from the main menu through the +prologue, which has already happened in any real save. + +**Do not clear that directory by hand.** It holds the player's actual +playthrough, and `rm -rf` on it — which is what these docs used to say — destroys +it. Use `accessibility/tests/run-testbed.sh`, which: + +1. **Moves** the save to `Alchemist.a11y-backup.` (never deletes it), +2. runs the testbed against the empty directory LÖVE then creates, +3. discards the test run's directory and moves the real save back, from an `EXIT` + trap so this happens on failure, on a crashed run and on Ctrl-C too. + +If the restore itself ever fails it says so loudly and leaves the backup on disk +rather than pressing on. + +Why a wrapper and not something inside the mod: `love.load` calls +`config.initialize`, `keybindings.initialize` and `user.initialize` long before +`main.lua` reaches `require("accessibility").initialize()`. By the time any of +our Lua runs, the save directory has already been read — the swap has to happen +around the process. + +Override the interpreter path with `LOVE=/path/to/lovec.exe` if yours differs. Runtime files the game also drops next to the source during dev (`config.json`, `user.json`, `keybindings.json`, savegames) are already in `.gitignore`. diff --git a/accessibility/docs/scratch.md b/accessibility/docs/scratch.md index c58f673..a60f2ea 100644 --- a/accessibility/docs/scratch.md +++ b/accessibility/docs/scratch.md @@ -2,12 +2,12 @@ **Read this first on any handoff.** Living document: update it whenever state changes, before ending a turn. -Last updated: 2026-08-02 · Currently: **Phases 0–5 COMPLETE. Handed off for -playtesting.** Remaining work is polish, mod settings and distribution. +Last updated: 2026-08-02 · Currently: **Phases 0–5 COMPLETE. In playtesting** — +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, **34/34 checks**. +verified by the testbed, **38/38 checks**. **Not set up yet, or unsure how the two trees relate?** → [00-dev-setup.md](00-dev-setup.md). @@ -15,13 +15,18 @@ verified by the testbed, **34/34 checks**. ### Run the testbed (do this after any change) ``` cd "D:/code/Sulphur Memories Alchemist 0.4.2 sanctified" -rm -rf ~/AppData/Roaming/LOVE/Alchemist # start from a clean save -"C:/Program Files/LOVE/lovec.exe" . --a11y-test +bash accessibility/tests/run-testbed.sh ``` +⚠ **Use the script, not `lovec . --a11y-test`.** The testbed needs an empty save +directory, and this used to be a bare `rm -rf ~/AppData/Roaming/LOVE/Alchemist` +in these docs — which ate Talon's actual playthrough. The script *moves* the save +aside, runs, and moves it back (restore is on an EXIT trap, and it never deletes +the real one). It can't be done inside the mod: `love.load` reads config, +keybindings and user data long before `main.lua` reaches our entry point. 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 +⚠ **The check COUNT varies between runs; 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.** @@ -565,6 +570,48 @@ invariant directly instead of trying to capture speech. **34/34 testbed checks** `In view: a chest, 1 tile south-west. a torch on the wall, 3, nearest 2 tiles south-west. …`. +### Done — inventory item action menu (bug fix, reported by Talon) + +**Symptom:** enter/`z` on an inventory item makes a click and then nothing is +spoken. Equipping the tutorial torch was therefore impossible to confirm. + +**Cause:** the click *is* the menu opening. `inventory.keypressed` → +`inventoryWindow.enableItemMenu()` → `modules/item/itemMenu.lua`, a **second, +nested selection** drawn as a small popup over the grid: the list of actions this +item currently allows, built by `modules/item/getActionsList.lua` from the +per-action `condition`s in `itemActions.lua` (Activate, Wield, Wear, Switch on, +Drink, Read, Open, Prepare for throwing…). Up/down move in it, enter activates, +**any other key cancels it**. None of this goes through `console.print` or the +cursor popup, so it was entirely silent. `TEsound.play(sound.select)` in +`enableItemMenu` was the click. **This is the game's only equipment UI** — there +is no separate equip screen — so the whole screen was unusable. + +**Fix** — `accessibility/states/_itemgrid.lua`: +- `window.itemMenu` is checked **first** and the grid cell stays quiet under it; + the menu is modal over the grid, so announcing the cell would be wrong. +- The menu is a closure with no getters, so `lines` and `currentSelectedAction` + are read as upvalues of its own `draw()`. +- Leads with the item name only when the menu just opened. The "just opened" + memory is **the menu table itself**, not a flag, which keeps `read()` + idempotent (see the journal warning above — a flag would be consumed by the + poll and the real announcement would come out headless). +- **`isEquipped` is now part of the cell text and of the focus key.** After + "Switch on" the menu closes with the cursor unmoved, so without it the re-read + would be suppressed as unchanged and the action would still feel silent. Now: + `Switch on` → `a torch, equipped, slot 3`. + +**Plus `accessibility/patches/itemmenu.lua`** (6th patch), wrapping the +`views/inventoryWindow` factory: when `getActionsList` returns nil — an item with +no applicable actions, e.g. a plain reagent — the game does *literally nothing*, +not even the click, which by ear is indistinguishable from a dropped keypress. +Says `, no actions`. Success is not announced here; the descriptor owns it. + +⚠ **Still not exercised against a real item.** The prologue inventory is empty, +so the testbed instead builds an `itemMenu` directly from a fake usable item and +asserts the two upvalue names still resolve — the version-bump canary for this +descriptor. **38/38 checks.** The live path (an actual torch, actually equipped) +is Talon's to confirm. + ### Known gaps (next up) - **Phase 5**: player status key (HP, statuses, current action), popup announcements (`popupStack.add`), damage alerts @@ -629,7 +676,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, 5 patches, 34/34 testbed checks**, +Phases 0–5 complete. **23 state descriptors, 6 patches, 38/38 testbed checks**, history unit tests passing, **one line changed in the game** (`main.lua`). Everything in the original brief is implemented: console speech with @@ -639,8 +686,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 -rm -rf ~/AppData/Roaming/LOVE/Alchemist # clean save -"C:/Program Files/LOVE/lovec.exe" . --a11y-test # ~4 min, 34 checks +bash accessibility/tests/run-testbed.sh # ~4 min, 38 checks git diff --stat vanilla-0.4.2 -- . ':!accessibility' ':!.gitignore' ':!CLAUDE.md' # must stay: main.lua | 1 + ``` @@ -672,8 +718,9 @@ 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. +- **Inventory item names, the `equipped` suffix and the item ACTION MENU** — + inventory is empty in the prologue, so only the "empty slot" path has run live. + The action-menu upvalue names are covered by a synthetic testbed check. - **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 diff --git a/accessibility/tests/run-testbed.sh b/accessibility/tests/run-testbed.sh new file mode 100644 index 0000000..3594835 --- /dev/null +++ b/accessibility/tests/run-testbed.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# +# Run the accessibility testbed against a clean save, WITHOUT destroying the +# player's real one. +# +# The testbed drives a fresh playthrough from the main menu, so it needs the +# LOVE save directory empty -- otherwise "Continue" appears, the new-game flow +# differs, and the prologue it navigates has already happened. That clean-out +# used to be a bare `rm -rf` in the docs, which silently ate a real save every +# time someone ran the tests. +# +# This cannot be done from inside the mod: love.load() reads config, keybindings +# and user data long before main.lua reaches our entry point, so by the time any +# Lua of ours runs the save directory has already been read. It has to happen +# around the process. +# +# So: MOVE the save aside (never delete it), run, then move it back. The test +# run's own save directory is discarded, leaving the tree exactly as it was. +# +# Usage: accessibility/tests/run-testbed.sh [extra love args...] + +set -uo pipefail + +LOVE="${LOVE:-C:/Program Files/LOVE/lovec.exe}" +GAME_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +# LOVE writes to /LOVE/; conf.lua sets identity "Alchemist". +SAVE_DIR="${APPDATA:-$HOME/AppData/Roaming}/LOVE/Alchemist" +BACKUP_DIR="${SAVE_DIR}.a11y-backup.$$" + +restored=0 + +restore() { + # Idempotent: this runs from the EXIT trap and possibly from a signal trap. + if [ "$restored" -eq 1 ]; then + return + fi + restored=1 + + # Throw away whatever the test run created, but only if we know it is ours + # (i.e. we successfully moved the real save out of the way first). + if [ -d "$BACKUP_DIR" ]; then + rm -rf "$SAVE_DIR" + if mv "$BACKUP_DIR" "$SAVE_DIR"; then + echo "[testbed] real save restored to $SAVE_DIR" + else + # Loud, and non-destructive: the save is still on disk, just parked. + echo "[testbed] !! COULD NOT RESTORE YOUR SAVE." >&2 + echo "[testbed] !! It is safe at: $BACKUP_DIR" >&2 + echo "[testbed] !! Move it back to: $SAVE_DIR" >&2 + exit 1 + fi + else + # There was no save to begin with; don't leave the test run's one behind + # to be mistaken for a real save later. + rm -rf "$SAVE_DIR" + fi +} + +# Checked BEFORE anything is moved, so a bad LOVE path can't leave the save +# parked under a backup name. +if [ ! -f "$LOVE" ]; then + echo "[testbed] LOVE not found at: $LOVE" >&2 + echo "[testbed] set LOVE=/path/to/lovec.exe" >&2 + exit 1 +fi + +trap restore EXIT +trap 'restore; exit 130' INT TERM + +if [ -e "$SAVE_DIR" ]; then + if ! mv "$SAVE_DIR" "$BACKUP_DIR"; then + echo "[testbed] could not move the save aside; refusing to run rather than" >&2 + echo "[testbed] risk your save. Is the game still open?" >&2 + exit 1 + fi + echo "[testbed] real save parked at $BACKUP_DIR" +else + echo "[testbed] no existing save to protect" +fi + +"$LOVE" "$GAME_DIR" --a11y-test "$@" +status=$? + +exit $status