The shot list becomes a config file, and the camera holds a phone

tools/scenes.json says WHAT to shoot; tools/clips.js only knows how. Every
knob Oliver asked for is a key: seconds, setting (gore/zombie), beat, court,
boss, weapons, movement, density, diversity, gore, games, screen — documented
in the file's own "_" block, validated in capture.py so a typo names itself
instead of coming back as fifteen seconds of empty court. Nine scenes ship in
the default list, across all four courts.

The frame is a PHONE now (393x852 -> 1080x2340), not 9:16: the portrait branch
derives its logical height from the canvas box, so a 9:16 take was never the
framing a player sees. The player has legs — held arrow keys through the real
axisX, so horde and flamer are no longer a statue with a flamethrower. The
crowd tops itself up where the game's own spawner puts it, diversity walks the
roster, and `gore` paints in the run's history a fifteen-second clip has no
time to earn. The pilot is picked by what the court is doing rather than by
config: tennis mode taps to swing, splatter draws the slingshot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-18 13:25:59 +02:00
1 parent 491f3b97f1
commit 472c86b36e
4 files changed
+350 -149

No files matched your search

+26 -15
View File
@@ -3553,27 +3553,37 @@ never an interstitial. An interstitial after death would land exactly in the "on
Clips the game already contains: the transition (the flagship), a boss walking on, the ball boy
calmly mopping blood, a flame ball starting an epidemic across a lined-up wave, parrying a boss's
volley back into him, a horde, the death + run summary.
**THE CAPTURE RIG IS BUILT (2026-08-18): `python3 tools/capture.py` and the folder fills up.**
`tools/capture.py` serves the repo with `tools/clips.js` injected, opens a throwaway-profile
Chrome, and the page stages each highlight through `window.T`, records it, POSTs the webm back;
ffmpeg writes 1080x1920 h264/aac into `promo/clips/`. Named scenes shoot alone
(`python3 tools/capture.py horde boss`). Seven scenes exist and the list in `clips.js` is the
place to add one — the gore side is `transition` (the flagship: four seconds of ordinary tennis
before the turn), `horde`, `flamer`, `mopboy`, `death`, and the zombie-tennis side is `boss` and
`match`. Nothing is faked for the camera: every clip enters through the same doors a player does
(`T.start`, `T.intoSplatter`, `startHorde`, `startBossFight`, `startMatch`), and the two pilots
are deliberately mediocre — the tennis one IS the reachability soak's from `test.js`.
Four things it learned the hard way, none of them obvious:
**THE CAPTURE RIG IS BUILT (2026-08-18), AND THE SHOT LIST IS DATA.** `python3 tools/capture.py`
fills `promo/clips/`; `python3 tools/capture.py horde boss` shoots two; `python3 tools/capture.py
my-shots.json` shoots another list entirely; `--seed 77` re-rolls every take. What to shoot lives
in `tools/scenes.json` — nine scenes today — and its `"_"` block documents every knob:
`seconds`, `setting` (gore / zombie), `beat` (transition / horde / boss / death / mopboy),
`court`, `boss`, `weapons`, `movement`, `density`, `diversity`, `gore`, `games`, `screen`.
`tools/clips.js` is the engine that knows how to shoot any of it: `tools/capture.py` serves the
repo with it injected, opens a throwaway-profile Chrome, and the page stages the scene, records
canvas + audio, POSTs the webm back; ffmpeg writes h264/aac. Nothing is faked for the camera —
every clip enters through the doors a player uses (`T.start`, `T.intoSplatter`, `startHorde`,
`startBossFight`, `startMatch`), the crowd walks in where the game's own spawner puts it, and the
player walks on held arrow keys through the real `axisX`. The two pilots are deliberately
mediocre (the tennis one IS the reachability soak's from `test.js`) and are chosen by what the
court is doing, not by config: `G.mode === 'tennis'` taps to swing, splatter draws the slingshot.
Six things it learned the hard way, none of them obvious:
- **The clip is the canvas BACKING STORE, not the window.** Chrome opened maximised and ignored
`--window-size`, and `resizeTo` did nothing; the frame is pinned instead by giving the canvas a
9:16 CSS box, which `render.js` measures (`cv.clientWidth`) through its ResizeObserver.
- **`TUNE.lowPower` defaults ON and caps `RES` at 3**, so the recording came out 810x1440 and
needed a blurry 1.33x upscale. The rig sets it to 0; with `--force-device-scale-factor=3` the
backing store is exactly 1080x1920 and no filter ever touches a pixel.
phone-sized CSS box, which `render.js` measures (`cv.clientWidth`) through its ResizeObserver.
- **THE SCREEN IS A PHONE, not 9:16** (Oliver, 2026-08-18): 393x852 like a 15-class iPhone, which
records at 1080x2340. The game genuinely lays out differently for it — the portrait branch
derives its logical height from the box — so a 9:16 take is not the framing a player sees.
`screen: "9x16"` is still there for a platform that insists.
- **`TUNE.lowPower` defaults ON and caps `RES` at 3**, so the first recordings came out 810 wide
and needed a blurry 1.33x upscale. The rig sets it to 0; with `--force-device-scale-factor=3`
the backing store IS the output and no filter ever resamples a pixel.
- **Audio has no master bus** — every voice in `audio.js` connects straight to `ac.destination` —
so the rig makes `destination` itself a gain node fanning to the speakers AND to a
`MediaStreamDestination`, patched on `BaseAudioContext.prototype` (not `AudioContext`'s) before
the game module boots. No refactor of the game for a tool that ships to nobody.
- **The court is remembered, not passed**: `main.js` reads `localStorage.court` once at module
init, so a scene's court rides the URL and is stored before the module boots.
- **One page load per scene.** A fresh boot is the only teardown guaranteed correct, and it costs
a second: the page reloads itself into the next scene and POSTs `/done` when the list is empty.
`tools/` is in neither the Dockerfile COPY nor the image, and `promo/clips/` is git- and
@@ -3581,6 +3591,7 @@ never an interstitial. An interstitial after death would land exactly in the "on
Still hand-work after the rig: the name mark on screen (item 5 wants it in EVERY clip), any
cutting, and the caption. Skipped deliberately — a burned-in watermark is a taste decision and
ffmpeg `drawtext` needs a font file the repo does not have in a `drawtext`-friendly format.
5. **How a viewer actually reaches the game — the name does the work, not the link.** TikTok and
Instagram have no clickable caption links, so the bio link costs two taps (profile, then link) and
converts in the single-digit percents. YouTube Shorts descriptions and pinned comments DO click,