Files
wuwiandClaude Opus 5 d71a26831a The game gets a shell: it buzzes in your hand, never turns sideways, and a swipe walks you back
A Capacitor iOS project (`npm run ios`), and the first three bullets of LATER item 2 built. The web
build is untouched and stays primary: every line here degrades to a no-op in a browser.

- HAPTICS ride the sound map, as the plan said they would: six call sites, all of them places an
  sfx already fires — your racket contact, the serve strike, `killZombie` (the one funnel every
  weapon routes through), `explode`, `hurtPlayer`, `killPlayer`, the weapon swap. The restraint
  rule lives in `haptics.js`, not at the call sites: a minimum gap per kind, so a bomb that kills
  twelve bodies is felt twice and the machine gun is a metronome rather than one long rumble.
  Pinned by a test. Untested on hardware — the simulator has no Taptic Engine.
- The plugin is reached through the INJECTED bridge, `Capacitor.nativePromise('Haptics', ...)`.
  `Capacitor.Plugins.Haptics` reads like the zero-dependency answer and is silently empty without
  @capacitor/core's registerPlugin — it would have fallen through to the web path inside the app
  and looked exactly like a phone with haptics switched off. No package JS ships.
- PORTRAIT is enforced where it can be: one orientation in Info.plist, so the app never rotates.
  That retires landscape as a surface rather than fixing it (OPEN 6). The web half is best-effort —
  `orientation` in both manifests and one guarded `screen.orientation.lock()`.
- SWIPE LEFT GOES BACK. The gesture finds the screen's one visible chevron and clicks it, so the
  shape of the wizard stays in the handlers that already own it. Not passive on purpose: a swipe
  inside a poster card also fires that card's click, and preventDefault is what stops the screen
  answering forward in the frame the chevron walked it back.
- The app has no server behind it, so `config.API` and one branch in `net.js` point the shell at the
  live host by name. Consequence: the app always talks to PROD, never staging.

`node test.js` green (105) over four seeds; builds, launches and reaches the native plugin on the
iOS 26.5 simulator; swipe-back walks the whole wizard in the browser pane.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 17:00:36 +02:00

17 lines
1.4 KiB
Markdown

# Splatter Tennis — session rules
1. **Read `PROJECT.md` first** — it is the only planning document: how Oliver's feedback is handled,
every invariant, every settled feel decision, what is open. Do not re-derive or contradict it.
2. **`node test.js` must pass before every commit.** It is also a Dockerfile build stage, so a red
suite blocks the deploy — but run it locally first. New state-machine behavior gets a test.
3. **Mode transitions live in `beats.js` only** (duel/horde/boss/changeover). One mode owns the
court; every `start*` refuses while another mode runs. A new mode starts there and refuses there.
4. **Every push deploys** (Coolify → splatter-tennis.wuwi.one). Bump `main.js?v=N` in index.html on
every push or phones keep the old build. New top-level files must be added to the Dockerfile COPY.
5. **Zero dependencies, no build step, no framework.** This is a settled decision — solve it with
what is already in the repo. The Capacitor shell (`package.json`, `ios/`) is the ONE exception
and it is outside `src/`: **no package JS ships.** Game code calls native plugins through the
injected bridge — `Capacitor.nativePromise(name, method, opts)`, see `src/haptics.js` — never an
`import` of a package (a bare specifier 404s on the web) and never `Capacitor.Plugins.*` (empty
without @capacitor/core, so it fails SILENTLY). `npm run ios` builds the app.