97 lines
5.3 KiB
Markdown
97 lines
5.3 KiB
Markdown
# RECORD STORE GUY engine spec — FMV QTE player
|
||
|
||
Pure static web app: no build step, no framework, no server logic. One HTML file,
|
||
one JS file, one CSS file, one JSON scene graph, a folder of mp4 clips. Must run
|
||
from `python3 -m http.server` locally and from nginx on the games VPS unchanged.
|
||
|
||
## Files
|
||
```
|
||
engine/
|
||
index.html shell: <video>, overlay divs, HUD
|
||
player.js all logic (~400 lines, vanilla ES6, no deps)
|
||
styles.css arcade look: scanlines, CRT vignette, neon HUD
|
||
scenes.json the scene graph (authored data, engine reads it verbatim)
|
||
clips/*.mp4 24fps 1280x720 h264+aac, named <scene_id>_<kind>.mp4
|
||
```
|
||
|
||
## scenes.json schema
|
||
```jsonc
|
||
{
|
||
"meta": { "title": "RECORD STORE GUY", "lives": 5, "version": 1 },
|
||
"start": "s01_entrance",
|
||
"scenes": {
|
||
"s01_entrance": {
|
||
"clip": "clips/s01_action.mp4", // the scene's action clip
|
||
"death": "clips/s01_death.mp4", // played on fail, then retry scene
|
||
"windows": [ // 1..n QTE windows within the clip
|
||
{
|
||
"t": [3.2, 3.9], // seconds: window open..close
|
||
"input": "right", // right|left|up|down|action (action = space/enter/click)
|
||
"cue": true // show the flashing direction cue at t[0]
|
||
}
|
||
],
|
||
"onSuccess": "s02_aisles", // next scene id, or "WIN"
|
||
"checkpoint": false // true = death respawns here, not act start
|
||
}
|
||
}
|
||
}
|
||
```
|
||
Rules the engine enforces:
|
||
- Input BEFORE the window opens = instant fail (Dragon's Lair rule — no button mashing).
|
||
Grace: inputs in the 250ms before t[0] fail; earlier inputs are ignored (idle fidget).
|
||
- No input by t[1] = fail. Wrong direction = fail.
|
||
- Multiple windows per clip run in sequence; all must pass.
|
||
- On fail: swap to death clip immediately (seek 0, play), then decrement lives.
|
||
Lives 0 → game-over screen → attract. Otherwise retry the scene (or last checkpoint).
|
||
- On success of last window: let action clip finish, then advance to onSuccess.
|
||
|
||
## Player loop (player.js structure)
|
||
1. **Boot**: fetch scenes.json → preload manifest.
|
||
2. **Preloader**: two `<video>` elements, A/B swap. While A plays scene N, B preloads
|
||
scene N's death clip first (most likely next need), then N+1's action clip, via
|
||
`preload="auto"` + `load()`. Also `fetch()` + blob-URL cache for clips ≤ ~8MB to
|
||
guarantee gapless swap. Show % bar on boot for first 3 clips only.
|
||
3. **Clock**: drive QTE timing off `video.currentTime` polled in `requestAnimationFrame`
|
||
(NOT wall-clock setTimeout — survives decode stutter/tab throttling).
|
||
4. **Input**: keydown (arrows + space/enter) and touch (swipe quadrants + tap = action).
|
||
Map gamepad later (Gamepad API, d-pad + A). Ignore key auto-repeat.
|
||
5. **Cue rendering**: at window open, flash a chunky neon arrow (direction) or ring
|
||
(action) center-low on screen, CSS animation ~300ms pulse. Dragon's Lair barely
|
||
cued; we cue by default — `cue:false` per window for hard mode later.
|
||
6. **HUD**: lives as record icons top-left, scene name bottom-left (debug only), score
|
||
= scenes cleared × 1000 minus deaths × 100, arcade font.
|
||
7. **States**: ATTRACT (loop attract.mp4 or title card + "PRESS ANY KEY", high-score
|
||
from localStorage) → PLAYING → DEATH → GAMEOVER → WIN (win.mp4 → credits → attract).
|
||
8. **Debug mode** (`?debug=1`): timeline scrubber, windows drawn as colored bands on a
|
||
bar under the video, current input state, frame-step keys (,/.), scene jump dropdown.
|
||
This is how we'll tune every window against real Flow clips — build it FIRST.
|
||
|
||
## Feel-tuning constants (top of player.js, tweak in one place)
|
||
```js
|
||
const WINDOW_DEFAULT = 0.7; // s — default window length if authoring shorthand used
|
||
const EARLY_GRACE = 0.25; // s before open where input = fail
|
||
const DEATH_HOLD = 0.6; // s to hold last death frame before retry fade
|
||
const CUE_LEAD = 0.0; // s to show cue before window opens (bump to 0.15 if too hard)
|
||
```
|
||
|
||
## Placeholder clips — tools/make_placeholders.sh
|
||
Generates test clips so the whole game is playable before any Flow spend. For each
|
||
scene in scenes.json: 5s 1280x720 clip, solid color per scene, huge scene-id text,
|
||
and at each QTE window a white flash + 1kHz beep + arrow glyph burned in at exactly
|
||
t[0]..t[1] (drawtext + sine + overlay enable='between(t,A,B)'). Death placeholders =
|
||
red background + "DEATH sXX". ffmpeg one-liner per clip; script reads scenes.json
|
||
with python3 -c json. Also make attract/gameover/win placeholders.
|
||
Ship scenes.json with 3 authored placeholder scenes so the loop is provable end-to-end.
|
||
|
||
## Post-processing real clips — tools/postprocess.sh
|
||
`postprocess.sh <in.mp4|dir> <scene_id> <action|death>` → trim silence/dead frames
|
||
(args for in/out points), scale/pad to 1280x720, 24fps, `-c:v libx264 -crf 20
|
||
-pix_fmt yuv420p -movflags +faststart`, silent aac track if none (uniform streams =
|
||
gapless swaps), output to engine/clips/ with canonical name, print duration so the
|
||
author can set windows. Keep originals in production/raw/ untouched.
|
||
|
||
## Deploy
|
||
rsync engine/ to botchat games VPS per deploy-map skill; it's static. Cache-bust
|
||
scenes.json + player.js with ?v= query from meta.version. Verify live by curling
|
||
the URL and loading in browser. ship-check before public link.
|