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:
1 parent
491f3b97f1
commit
472c86b36e
4 files changed
+350
-149
No files matched your search
+26
-15
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user