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>
5.9 KiB
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)toserver/auth.ts: returnauthClaims.subwhen present, else a single fixed devTEST_USER_ID. LeaverequireAdminand the symmetric-secret admin path untouched. - Note in code that the external
glitch.universitykey-exchange path replaces only the fallback branch later; theuser_idcolumn does not change.
2. Migration 015_narrative_layer.sql
Minimal tables to run one scripted scene against one chapter.
mysteries (id, slug UNIQUE, title)andmystery_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 (reuseinstantiate_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 — requestedpose_key→ NPCdefault_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'suser_idis not the caller.
5. Manifest importer (content path)
- Extend
MysteryManifestinscripts/importMysteryTemplate.tswithcast[](NPCs, each withdefaultPoseandposes: { pose_key → asset path }) andcutscenes[]({ slot, chapter?, steps: [{ npc, pose, text }] }). - Freeze a
mysteriesrow + onemystery_chaptersentry 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_introcutscene tomysteries/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 whencurrentreturns a playthrough.DialogueOverlay: renderpendingCutscenesteps; 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-motionrespected; on finish call…/seenthen reveal the board.- App boot:
GET …/current→ no playthrough shows splash; New Game POSTs, loads the returned level (reuse existing level fetch/render), and playspendingCutscenebefore handing control to the board.
7. Tests
- Unit:
resolvePoseacross requested / default / none;getPendingCutscenereturns only unseen scenes. - Integration: New Game creates a playthrough bound to the test user;
currentreturns it;seenis 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_dialogueis cutscene-level, not step-level (no mid-scene resume yet).- One migration
015holds all slice tables rather than several.