Files
gupi-osint-board/docs/narrative-slice-1.md
gitprovandClaude Opus 4.8 ddb3a386f0 Add story-graph narrative system (campaigns, editors, runtime)
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>
2026-08-18 15:37:46 +02:00

5.9 KiB
Raw Permalink Blame 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_keynull (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.