# 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.

## Branching / choice windows (NEW — required by THE SHIFT scenes in WORLDS.md)
A window may replace `input` with `choices`, mapping inputs to outcomes:
```jsonc
{ "t": [2.5, 4.0], "cue": true,
  "choices": {
    "left":  { "goto": "sh1_house_ok" },          // proceed/branch to scene
    "up":    { "death": "clips/sh1_death_pit.mp4",
               "goto": "misfile_dimension" },      // custom death THEN branch (pit!)
    "right": { "death": "clips/sh1_death_acct.mp4" } // custom death, normal retry
  } }
```
Rules: cue shows all choice glyphs side by side; no-input-by-close uses the scene's
default death; a `choices` window must be the clip's last window; `random: ["a","b"]`
on a scene picks one of two clip/outcome sets per attempt (for THE TAPE mimic gag).
Choice windows default to longer durations (1.5s) — they're decisions, not reflexes.

## v2 mechanics (spec'd in DESIGN.md — build after Phase 3)
- `mirror: true` scene flag: CSS `transform: scaleX(-1)` on the video + swap
  left/right in input handling and cues. Free content doubling (Dragon's Lair trick).
- Replay queue: scenes died-in (cap 8) replay before the final track.
- Difficulty tiers: easy = skip scenes flagged `skippableOnEasy` + windows +0.2s;
  hard = `cue:false` everywhere after first visit.
- `needledrop: {t, clip, bonus}` on a scene: optional action input during a flash
  window branches to an alternate clip; ignoring it is not a fail.
- `collect: {t, input}` on a scene: unprompted input during a background flash sets
  a flag (white labels); wrong unprompted inputs outside windows stay ignored.
- Endurance windows: `onMiss: "restart"` (replay clip, tighter windows), `maxRestarts`.
- Hold windows: `hold: true` — input must be held from window open to close.

## 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.
