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.
|