// Teaser timeline: the single source of truth for the picture (teaser.js) and the music (music.js). // Everything is a pure function of time, so a frame at t always looks the same and the hits land on the cuts. // The terminal text is scripted, modelled on real storebound output for the demo game (examples/demo-game, // placeholder ids, dry runs). Nothing here talks to a store. export const BPM = 128; export const BEAT = 60 / BPM; // 0.46875 s export const BAR = BEAT * 4; // 1.875 s export const FPS = 30; export const BARS = 20; export const DURATION = BARS * BAR; // 37.5 s /** Scenes in bars; start/end in seconds are derived below. */ const SCENE_BARS = [ ['prompt', 2], ['config', 2], ['prices', 2], ['languages', 2], ['questionnaires', 2], ['shots', 3], ['validate', 2], ['submit', 2], ['brand', 3], ]; let acc = 0; export const SCENES = SCENE_BARS.map(([id, bars]) => { const s = { id, start: acc * BAR, end: (acc + bars) * BAR, bars }; acc += bars; return s; }); export const scene = id => SCENES.find(s => s.id === id); export const PROMPT = 'ship Prism Puddle to Google Play and the App Store'; export const PROMPT_AT = 0.35; // seconds export const PROMPT_CPS = 21; // the user types export const CMD_CPS = 62; // the agent types export const LANGS = ['en-US', 'tr-TR', 'de-DE', 'fr-FR', 'es-ES', 'es-419', 'it-IT', 'pt-BR', 'ja-JP', 'ko-KR', 'zh-CN', 'ru-RU', 'ar', 'hi-IN']; // Price rows for the flicker panel: illustrative local price points for a 2.99 / 0.99 USD pair (Play converts from // USD per region; TR is a config override, as in templates/storebound.config.ts). export const PRICES = [ ['US', 'USD', '2.99', '0.99'], ['TR', 'TRY', '99.99', '34.99'], ['DE', 'EUR', '2.99', '0.99'], ['GB', 'GBP', '2.79', '0.89'], ['JP', 'JPY', '450', '150'], ['BR', 'BRL', '14.90', '4.90'], ['IN', 'INR', '249', '79'], ['KR', 'KRW', '4,100', '1,300'], ['MX', 'MXN', '59.00', '19.00'], ['ID', 'IDR', '45,000', '15,000'], ['SA', 'SAR', '10.99', '3.69'], ['FR', 'EUR', '2.99', '0.99'], ['CA', 'CAD', '3.99', '1.39'], ['AU', 'AUD', '4.49', '1.49'], ]; export const BLOCKERS = [ ['apple', 'build 12 is PROCESSING'], ['apple', 'ja: missing keywords'], ['apple', 'remove_ads: MISSING_METADATA'], ['apple', 'no app price schedule'], ['play', 'tr-TR: no listing'], ['play', 'drops_500: not on Play'], ['play', 'no release on any track'], ]; export const BLOCKERS_AT = 0.45; // first blocker line, relative to the validate scene export const BLOCKER_GAP = 0.05; export const FIX_AT = 1.05; // first flip to ok export const FIX_GAP = 0.26; /** * Terminal lines per scene. at = seconds after the scene start. * kind: cmd (typed by the agent), out (printed), note (agent comment), ok / bad (coloured output). */ const T = { prompt: [ { at: 2.85, kind: 'note', text: 'agent using storebound · dry runs first' }, ], config: [ { at: 0.0, kind: 'cmd', text: 'bunx storebound init' }, { at: 0.45, kind: 'out', text: '[init] create storebound.config.ts' }, { at: 0.55, kind: 'out', text: '[init] create store/listing.md' }, { at: 0.65, kind: 'out', text: '[init] create store/appstore.md' }, { at: 0.75, kind: 'out', text: '[init] create store/declarations.md' }, { at: 0.85, kind: 'out', text: '[init] create store/app-privacy.md' }, { at: 0.95, kind: 'out', text: '[init] create store/privacy-policy.html' }, { at: 1.875, kind: 'cmd', text: 'bunx storebound check' }, { at: 2.3, kind: 'out', text: '[check] config ok: storebound.config.ts' }, { at: 2.42, kind: 'out', text: '[check] play listing: 14 languages' }, { at: 2.54, kind: 'out', text: '[check] appstore listing: 14 locales' }, { at: 2.66, kind: 'out', text: '[check] data safety: 764 CSV rows' }, { at: 2.78, kind: 'out', text: '[check] shots copy: 14 languages x 4 scenes' }, ], prices: [ { at: 0.0, kind: 'cmd', text: 'bunx storebound play products --dry' }, { at: 0.62, kind: 'out', text: '[play] 1/2 remove_ads: create · 173 regions (dry)' }, { at: 0.9, kind: 'out', text: '[play] 2/2 drops_500: create · 173 regions (dry)' }, { at: 1.4, kind: 'cmd', text: 'bunx storebound apple iap --dry' }, { at: 1.95, kind: 'out', text: '[apple] 1/2 remove_ads: missing (dry)' }, { at: 2.07, kind: 'out', text: '[apple] 2/2 drops_500: missing (dry)' }, ], languages: [ { at: 0.0, kind: 'cmd', text: 'bunx storebound play listing --dry' }, { at: 0.6, kind: 'out', text: `[play] 14 listings: ${LANGS.slice(0, 4).join(', ')} ...` }, ...LANGS.map((l, i) => ({ at: 0.8 + i * 0.13, kind: 'out', text: `[play] ${String(i + 1).padStart(2)}/14 ${l}` })), ], questionnaires: [ { at: 0.0, kind: 'cmd', text: 'bunx storebound play data-safety --dry' }, { at: 0.62, kind: 'out', text: '[play] data safety: 6 data types declared' }, { at: 0.95, kind: 'cmd', text: 'bunx storebound apple appinfo --dry' }, { at: 1.55, kind: 'out', text: '[apple] categories: GAMES / GAMES_CASUAL (dry)' }, { at: 1.67, kind: 'out', text: '[apple] age rating declaration: all NONE (dry)' }, { at: 1.85, kind: 'note', text: '[apple] appinfo done (App Privacy is UI-only: store-declarations skill)' }, ], shots: [ { at: 0.0, kind: 'cmd', text: 'bunx storebound shots render' }, ...['play', 'tab7', 'tab10', 'ios69', 'ios65', 'ipad13', 'feature'].flatMap((d, k) => ['en', 'tr', 'ja', 'ar'].map((l, j) => ({ at: 0.5 + (k * 4 + j) * 0.14, kind: 'out', text: `[shots] store/renders/${l}/${d}/${d === 'feature' ? '1024x500' : '01'}.png` }))), ], validate: [ { at: 0.0, kind: 'cmd', text: 'bunx storebound validate' }, // the blocker lines are drawn by teaser.js from BLOCKERS (they change state) ], }; export const TERMINAL = T; /** Seconds at which each typed character lands (for key clicks in the music). */ export function keyTimes() { const out = []; for (let i = 0; i < PROMPT.length; i++) out.push(PROMPT_AT + i / PROMPT_CPS); for (const s of SCENES) for (const l of T[s.id] ?? []) { if (l.kind !== 'cmd') continue; for (let i = 0; i < l.text.length; i++) out.push(s.start + l.at + i / CMD_CPS); } return out; } /** Seconds of the scene cuts that get a hit (every scene after the first). */ export const CUTS = SCENES.slice(1).map(s => s.start);