recordstoreguy/ENGINE.md
type-two 4f7d21df2b Engine v2 + full 45-scene graph: choices, random, endurance, hold, needledrop, white labels, replay queue, stinger
- scenes.json: every DESIGN.md scene wired — T1-T7, SH1-6, Misfile, 4 needle-drop
  alts, 7 white labels, boss volleys, REPLAY queue sentinel, win+stinger gate
- player.js: choice windows (branch/death-goto), random variants (sh2 tape),
  endurance restarts w/ tightening (sh4), hold windows (t4s5/t5s5), needledrop
  power-up branches, unprompted collects, mirror-on-replay, soft fail (t5s4),
  needle_skip transitions, stale-death-timeout guard
- clips renamed to DESIGN ids (s01->t1s1 etc); placeholder gen covers all new
  window types and never overwrites real footage (86 clips: 8 real + 78 gen)
- sheets: added missing sh4_action + t5s4_action prompts
- verified in browser: choice pass/timeout, collect, needledrop, endurance
  tighten+death, hold pass/release-fail, soft fail, replay queue w/ mirror,
  win->stinger with 7 labels, live-timing run of t1s1

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 04:40:41 +10:00

136 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 (BUILT 2026-07-20 — as implemented in player.js)
- `mirrorOk: true` scene flag: scene is mirror-eligible. The engine mirrors it
(CSS scaleX(-1) + left/right swap in inputs and cues) when it comes back in the
replay queue — free variety, "the Dead Wax remembers... backwards".
- Replay queue: `onSuccess: "REPLAY"` (on sh6) queues died-in scenes (unique,
cap 8, choice/random scenes excluded), needle_skip.mp4 transition between each,
then t7s1. Needledrop/collect are disabled during replays.
- `needledrop: {t, clip, bonus, label?, skipTo?}` on a scene: optional action
input during the flash branches to the alt clip; ignoring it is not a fail.
`label` grants a white label, `skipTo` overrides onSuccess (t6s1 alt skips t6s2).
- `collect: {t, input, label}` on a scene: unprompted input during a background
flash grabs white label N (+250, toast); wrong unprompted inputs stay ignored.
All 7 labels → stinger.mp4 plays after win.mp4.
- Endurance: `onMiss: "restart"` + `maxRestarts` on a scene — misses replay the
clip with window closes tightened 0.15s per restart; the Nth miss is a real death.
- Hold windows: `hold: true` — input held from open (0.35s slack) to close.
- Soft fail: `failGoto: "id"` on a scene — fail costs no life, plays the death
clip if any, and respawns at the given scene (t5s4 platter lock → t5s1).
- Choice deaths can branch: `{death, goto}` respawns at goto after the death
(sh1 wrong file → Misfile Dimension). Scene-level `score`/`label` grant on clear
(sh5_bonus). Choice window timeout uses the scene's default death.
- NOT built (cut for now): difficulty tiers, gamepad, touch-hold windows.
## 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.