recordstoreguy/ENGINE.md
m3ultra 11e4c557b2 DESIGN.md: full cast/map/scene table/puzzle spec + laserdisc research; ENGINE v2 mechanics
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:02:11 +10:00

126 lines
7.0 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 (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.