# First slice: splash → New Game → briefing → board Scope: the thinnest end-to-end narrative thread. A player lands on a **PRINCIPAL INVESTIGATOR** splash, clicks **New Game**, watches one scripted briefing cutscene from the Glitch University professor (with a pose swap), and arrives on the existing Glass Harbor board. Identity is a hard-coded test user. All cutscene content is authored through the manifest importer — nothing hard-coded in React. Explicitly **out of scope** for this slice: the admin authoring panel, multiple chapters, the debrief/back-to-board scenes, dialogue branching, and any LLM. The schema below only reserves those (`kind`, nullable `advances_to`) — it does not build them. ## Ordered tasks ### 1. Identity: hard-coded test user (do this first) - [ ] Add `resolveUserId(req)` to `server/auth.ts`: return `authClaims.sub` when present, else a single fixed dev `TEST_USER_ID`. Leave `requireAdmin` and the symmetric-secret admin path untouched. - [ ] Note in code that the external `glitch.university` key-exchange path replaces only the fallback branch later; the `user_id` column does not change. ### 2. Migration `015_narrative_layer.sql` Minimal tables to run one scripted scene against one chapter. - [ ] `mysteries (id, slug UNIQUE, title)` and `mystery_chapters (mystery_id, chapter_index, level_template_version_id, PK (mystery_id, chapter_index))`. - [ ] `npcs (id, mystery_id, name, role, default_pose_key)`. - [ ] `poses (id, npc_id, pose_key, asset_id → osint.assets, UNIQUE (npc_id, pose_key))`. - [ ] `cutscenes (id, mystery_id, chapter_index NULLABLE, slot, title)`. - [ ] `dialogue_steps (id, cutscene_id, step_key, sort_order, npc_id, pose_key, kind DEFAULT 'scripted', text, advances_to NULLABLE, UNIQUE (cutscene_id, sort_order))`. - [ ] `playthroughs (id, user_id, mystery_id, current_chapter_index, current_level_id, created_at)`. - [ ] `seen_dialogue (playthrough_id, cutscene_id, seen_at, PK (playthrough_id, cutscene_id))`. (Cutscene-level for the slice; step-level deferred.) ### 3. Repository (`server/narrativeRepository.ts`, or extend levelRepository) - [ ] `createPlaythrough(userId, mysterySlug)`: instantiate chapter 1's template version into a fresh level (reuse `instantiate_template_version`), insert the playthrough, return it. - [ ] `getCurrentPlaythrough(userId)`: newest unfinished playthrough for the user. - [ ] `getPendingCutscene(playthrough)`: earliest unseen cutscene matching the current slot/chapter, with steps joined to NPC + resolved pose asset URL. - [ ] `resolvePose(npc, poseKey)`: pure, unit-testable — requested `pose_key` → NPC `default_pose_key` → `null` (no artwork). Return the asset URL or null. - [ ] `markCutsceneSeen(playthroughId, cutsceneId)`: idempotent insert. ### 4. API (`server/index.ts`), all scoped to `resolveUserId(req)` - [ ] `POST /api/playthroughs` → create for the user, returns playthrough + `pendingCutscene` + current level id. - [ ] `GET /api/playthroughs/current` → the user's playthrough or 204/empty. - [ ] `POST /api/playthroughs/:id/cutscenes/:cutsceneId/seen` → idempotent; 403 if the playthrough's `user_id` is not the caller. ### 5. Manifest importer (content path) - [ ] Extend `MysteryManifest` in `scripts/importMysteryTemplate.ts` with `cast[]` (NPCs, each with `defaultPose` and `poses: { pose_key → asset path }`) and `cutscenes[]` (`{ slot, chapter?, steps: [{ npc, pose, text }] }`). - [ ] Freeze a `mysteries` row + one `mystery_chapters` entry pointing at the Glass Harbor template version, plus the cast and the intro cutscene, through the same authenticated operations already used for assets. - [ ] Tolerate a cast with **no pose images** (author an NPC with just a name/role so the slice runs art-free); upload images only if the manifest provides them. - [ ] Add a `mystery_intro` cutscene to `mysteries/glass-harbor/mystery.json`: the professor briefing, 3–5 scripted steps, referencing pose keys that may not exist yet (fallback handles it). ### 6. Frontend - [ ] `SplashScreen`: title **PRINCIPAL INVESTIGATOR** over "Glitch University" in the existing boot aesthetic; **New Game** always, **Resume** when `current` returns a playthrough. - [ ] `DialogueOverlay`: render `pendingCutscene` steps; portrait from the resolved pose (fallback to name+text when null); advance on click/Space/Enter; typewriter with instant-reveal on first press; `prefers-reduced-motion` respected; on finish call `…/seen` then reveal the board. - [ ] App boot: `GET …/current` → no playthrough shows splash; New Game POSTs, loads the returned level (reuse existing level fetch/render), and plays `pendingCutscene` before handing control to the board. ### 7. Tests - [ ] Unit: `resolvePose` across requested / default / none; `getPendingCutscene` returns only unseen scenes. - [ ] Integration: New Game creates a playthrough bound to the test user; `current` returns it; `seen` is idempotent; a second user id cannot read or mark the first user's playthrough. - [ ] Integration: importing the extended manifest freezes the NPC + intro cutscene, and a New Game surfaces exactly those steps. - [ ] (Optional) Browser smoke: splash → New Game → overlay appears and advances → board visible; reload does not replay the seen briefing. ## Definition of done (slice) Against the test user, New Game creates a playthrough, the professor's scripted briefing plays with a pose swap (or clean name+text when art is absent), the Glass Harbor board loads, and a reload resumes without replaying the briefing — with the cutscene authored via the manifest, not hard-coded. ## Open defaults (flag before coding if you disagree) - Second New Game **resumes** an unfinished playthrough (explicit Restart), rather than always starting fresh. - `seen_dialogue` is **cutscene-level**, not step-level (no mid-scene resume yet). - One migration `015` holds all slice tables rather than several.