- 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>
136 lines
7.7 KiB
Markdown
136 lines
7.7 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.
|
||
|
||
## 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.
|