recordstoreguy/ENGINE.md
m3ultra 9a5ed2d02b Record Store Guy: master plan, worlds design, engine spec, production bible
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:39:11 +10:00

5.3 KiB
Raw Blame History

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

{
  "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)

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.