Files
gupi-osint-board/docs/narrative-slice-1.md
T

105 lines
5.9 KiB
Markdown
Raw Normal View History

# 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, 35 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.