Split The code is public; the app answers only to the crew.

Motion you did not ask for: honour prefers-reduced-motion where it is decoration, and nowhere else #217

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`settings.ts` gains `motion: "full" | "reduced"`, guarded exactly like `difficulty`, with a new `src/client/motion.ts` owning the two values, their labels and the media query. `parseSettings` takes the system's answer as an optional `unchosen` fallback and reads a stored `motion` first, so `prefers-reduced-motion` can only ever supply a first-run default — a choice already made wins outright in bo…

shipped merged

opened this

settings.ts gains motion: "full" | "reduced", guarded exactly like difficulty, with a new src/client/motion.ts owning the two values, their labels and the media query. parseSettings takes the system's answer as an optional unchosen fallback and reads a stored motion first, so prefers-reduced-motion can only ever supply a first-run default — a choice already made wins outright in both directions, and loadSettings is tested end to end against stubbed localStorage and matchMedia to prove it. The switch is a Motion select in the menu and again in the pause overlay, paired and live the way Difficulty is; drawHud takes one motion value after alpha and decides once a frame, and the renderer takes it as a number through setMotion so the hot loop has no second path.

Reduced turns down two things and only two: the walk's sway on the gun in your hands (bobScale goes to 0, which draws exactly the gun a player standing still already holds — the renderer test pins that equivalence to the last float), and the damage wedge's fade, which now holds its strength for its life and then goes, keeping the direction and the weight of the hit. Everything else is byte-identical on both settings: a HUD test draws a frame with a hit marker, a kill-feed line and an open crosshair at both motions and asserts the whole call list is equal, and a renderer test shows the kick, the reload dip and the muzzle flash all survive reduced motion, because they are things that happened rather than idle motion.

Three items on the issue's list turned out not to exist in this tree, and I pinned them rather than inventing motion to remove: the hurt vignette has no clock in it at all — its alpha is a function of view.health alone — so it is already the steady tint reduced motion would ask for, and a test asserts its alpha does not move with world.time on either setting while still deepening as you are shot. The spectate camera is already a cut: main.ts builds it from eyePos(watched(...)) with nothing interpolated. And I deliberately left the muzzle flash alone on both settings — removing your own flash would clear the view while spraying, which is an aim advantage, and the issue's own rule is that any change making the game easier under reduced motion is the wrong change. The README's Playing section says all of this, including the sentence about why the recoil punch stays, and readme.test.ts holds it there.

To try it: open the preview, set Motion: Reduced in the menu (or pause mid-match), walk with a rifle out and watch the gun stop swaying while the recoil, the flinch and the crosshair are untouched; take a hit and watch the wedge hold instead of sliding away. On a machine with reduced motion on at the OS level, a fresh browser profile starts on Reduced with no stored settings, and stays wherever you put it afterwards. bun run check is green: 779 tests, 0 failures.

🤖 Generated with Claude Code

Closes #77

commented

probe

Approved

Approved — the full review is the anchored comment on src/client/motion.ts.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs