Introduce the narrative layer as a directed story-flow graph: an authored campaign a player walks node by node, replacing the interim slot/chapter model. Schema (migrations 015-021): - mysteries, global NPC templates + named poses, per-user playthroughs - story_nodes, terminals, utterances (the flow graph and dialogue trees) - clean cutover: retire slot cutscenes/chapters/seen_dialogue Runtime: - New Game creates a playthrough bound to the JWT identity (dev test-user fallback) - advance() walks the graph cutscene -> dialogue -> level -> ..., auto-skipping gates - branching dialogue: player choices route out through node terminals Admin authoring: - NPC editor: upload named poses to the gupi MinIO bucket - mystery graph editor: vertical node canvas, wiring, entrypoint, delete-by-click - dialogue crafter: utterance tree, Tab to add child, 1/2 speaker, undo Content authored via the manifest importer / admin panel and seeded for Glass Harbour. MinIO added to the dev stack; dev container runs in development mode. Also includes a folder-widget simplification (removes open/close) and a resolveUserId auth helper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
105 lines
5.9 KiB
Markdown
105 lines
5.9 KiB
Markdown
# 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.
|