From ce91b4ce98dae5a636544e7f684ade0bbdaa7a20 Mon Sep 17 00:00:00 2001 From: slaguru666 <111923774+slaguru666@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:12:39 +0100 Subject: [PATCH] feat: port all six scenarios' clue trails into the data model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Populate clues[] for every Continuum scenario (afterimage, day-one, vain-crown, silvery-moon, chopper, princes-bride), transcribed faithfully from the gm-utility console sources (CLUES arrays, gbclue GM-brief spines, and the princes-bride module). The Slice 4 clue safety-net now works against real content — essential-gap banner, act labels, and fallbacks. day-one and princes-bride carry the source's own essential/optional split; the four GM-brief spines mark each tracked clue essential (single curated list, no optional tier in-source). tests/scenarios/clues.test.js guards shape, unique ids, and solvability. 129 tests pass; build clean. Co-Authored-By: Claude Opus 4.8 --- README.md | 2 +- .../plans/2026-07-20-director-slice5.md | 9 +++- src/scenarios/afterimage.js | 9 +++- src/scenarios/chopper.js | 10 +++- src/scenarios/day-one.js | 15 +++++- src/scenarios/princes-bride.js | 17 ++++++- src/scenarios/silvery-moon.js | 11 ++++- src/scenarios/vain-crown.js | 9 +++- tests/scenarios/clues.test.js | 46 +++++++++++++++++++ 9 files changed, 120 insertions(+), 8 deletions(-) create mode 100644 tests/scenarios/clues.test.js diff --git a/README.md b/README.md index fa439a1..c824e57 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ compress if you reach a beat behind schedule. 3. **Slice 3** — dice rule-packs (CoC d100, VANITY d6-pool, Panic & Glory, Dee Sanction) + tray pack selector. ✅ 4. **Slice 4** — the clue **safety-net** (essential-clue gap tracker + fallbacks). ✅ 5. **Slice 5** — the convention **hub** (all slots, live "live now / up next / done", deep-links into each scenario). ✅ All **six** Continuum 2026 slots ported and scheduled with real times. -6. **Later** — remaining tray tools (break timer, parking-lot note, wake-lock); online art "Generate"; porting the scenarios' clue trails / cast into the data model; a dedicated BRP dice pack (Day One currently borrows the d100 pack); markdown → scenario-data generator; native iPad wrapper. +6. **Later** — remaining tray tools (break timer, parking-lot note, wake-lock); online art "Generate"; porting the scenarios' cast into the data model (clue trails done); a dedicated BRP dice pack (Day One currently borrows the d100 pack); markdown → scenario-data generator; native iPad wrapper. ## Docs diff --git a/docs/superpowers/plans/2026-07-20-director-slice5.md b/docs/superpowers/plans/2026-07-20-director-slice5.md index 0e5ba88..91ef684 100644 --- a/docs/superpowers/plans/2026-07-20-director-slice5.md +++ b/docs/superpowers/plans/2026-07-20-director-slice5.md @@ -52,7 +52,14 @@ jsdom. No new dependencies. Fully offline. - Correction found while porting: the real slate is **six** slots (Slot 5 *Silvery Moon* was missing from the first hub draft), and "Another Fine Mess" is *Get to the Chopper — Another Fine Mess*. +- **All six scenarios' clue trails ported** into the data model (transcribed from the console + `CLUES`/`gbclue`/module sources via `extract-clues.mjs`), so the Slice 4 safety-net now works + against real content. `tests/scenarios/clues.test.js` guards shape + solvability. The four + GM-brief-sourced trails (afterimage, vain-crown, silvery-moon, chopper) mark every tracked + clue `essential: true` (their source is a single curated spine); day-one and princes-bride carry + the source's own essential/optional split. + ## Still deferred (backlog) -- Porting each scenario's **clue trails / cast** into the data model (shipped `clues: []` like AFTERIMAGE). +- Porting each scenario's **cast** into the data model (still `cast: []`). - A dedicated **BRP** dice pack — Day One currently borrows `coc-d100` (d100 roll-under). diff --git a/src/scenarios/afterimage.js b/src/scenarios/afterimage.js index 341b6a4..c2cf384 100644 --- a/src/scenarios/afterimage.js +++ b/src/scenarios/afterimage.js @@ -8,7 +8,14 @@ export default { { id: 'a3-pier', act: 3, label: 'Pier confrontation', targetMin: 175, hardTrigger: true }, { id: 'epilogue', act: 4, label: 'Baseline / ambiguous end', targetMin: 200 }, ], - clues: [], + clues: [ + { id: 'c1', label: 'The door-cam face that cannot exist (hand the still around as an object)', essential: true }, + { id: 'c2', label: 'The ledger: A-1 · A-2 · A-3 — and A-3’s file just reads “retired”', essential: true }, + { id: 'c3', label: 'The identical childhood, recited twice across the split', essential: true, fallback: 'the monologue cards ×2' }, + { id: 'c4', label: 'The spike was intimate — face to face, not an execution (Avis)', essential: true }, + { id: 'c5', label: 'Pier + dawn deadline known before 2:45', essential: true, fallback: 'both Act Two tracks carry it' }, + { id: 'c6', label: 'Whisper-9 is stolen Wallace property — why Corvin has legal cover', essential: true }, + ], cast: [], props: [], }; diff --git a/src/scenarios/chopper.js b/src/scenarios/chopper.js index fe02ef2..2263c06 100644 --- a/src/scenarios/chopper.js +++ b/src/scenarios/chopper.js @@ -13,7 +13,15 @@ export default { { id: 'esc', label: 'ESCAPE SEQUENCE — four phases, everyone rolls, blades', targetMin: 160, hardTrigger: true }, { id: 'epi', label: 'Epilogue — beers, the satellite photo, "finish your beers"', targetMin: 170 }, ], - clues: [], + clues: [ + { id: 'c1', label: 'The tannoy countdown — “test at thirteen hundred”', essential: true, fallback: 'the patrol radio, Act One' }, + { id: 'c2', label: 'The shrines face INLAND — apologising to the sea', essential: true }, + { id: 'c3', label: 'Concha’s warning: they are fishing, with a very big hook', essential: true, fallback: 'Willis’s cell wall: “they keep singing about a wedding”' }, + { id: 'c4', label: 'The empty plans tube — AL VOLCÁN, this morning', essential: true, fallback: 'Volkova’s office if Beat 3 is cut' }, + { id: 'c5', label: 'Segal’s mandala = the array layout (the reward for fixing him properly)', essential: true }, + { id: 'c6', label: 'THE BIRDS STOP — the gear change, marked plainly (SAN 0/1D2)', essential: true }, + { id: 'c7', label: 'While 3+ singers hold the note, the console alone cannot stop it', essential: true }, + ], cast: [], props: [], }; diff --git a/src/scenarios/day-one.js b/src/scenarios/day-one.js index 7ec9e35..215c1c8 100644 --- a/src/scenarios/day-one.js +++ b/src/scenarios/day-one.js @@ -14,7 +14,20 @@ export default { { id: 'act3', label: 'Act Three done — cost paid', targetMin: 195 }, { id: 'epi', label: 'Epilogue + debrief done — tangent seeded', targetMin: 210 }, ], - clues: [], + clues: [ + { id: 'c1', label: 'High-vis man was mid-breakfast when he stopped — onset is rapid, no warning stage.', essential: true, act: 'Act One' }, + { id: 'c2', label: 'Callum’s radio: "first reports… Elephant… 07:14" — more than three hours before the alert. The government knew.', essential: true, act: 'Act One', fallback: 'as the Market breaks, his radio crackles and he repeats the fragment aloud.' }, + { id: 'c3', label: 'Drone stream on social media — north bank panicking too; not localised.', essential: false, act: 'Act One' }, + { id: 'c4', label: 'BBC on the stall radio: "health emergency — do not approach persons showing disorientation." Evasive language.', essential: false, act: 'Act One' }, + { id: 'c5', label: 'Danny’s bite in the Blue Anchor — infection is already inside the "safe" space.', essential: true, act: 'Act Two', fallback: 'if nobody checks him, he turns in front of the group.' }, + { id: 'c6', label: 'Marcus’s drone stream — aerial London. City-wide. Much worse than they thought.', essential: true, act: 'Act Two' }, + { id: 'c7', label: 'Radio: "cordon… north bank… all units hold position" — south of the river is written off.', essential: true, act: 'Act Two', fallback: 'Marcus’s chat — "military on Embankment going NORTH not south — wtf".' }, + { id: 'c8', label: 'Helen is ex-Cabinet Office. "There’s a protocol for this. If they haven’t activated it, something went wrong before the alert." Names Meridian.', essential: false, act: 'Act Two' }, + { id: 'c9', label: 'Priya’s editor had Meridian whistleblower documents 48 hours ago. He didn’t run the story.', essential: false, act: 'Act Two' }, + { id: 'c10', label: 'Ryan’s bite in the pharmacy — post-alert bites already symptomatic. It is accelerating.', essential: false, act: 'Act Two' }, + { id: 'c11', label: 'Pete’s radio confirms the north-bank cordon — closes the loop.', essential: false, act: 'Act Three' }, + { id: 'c12', label: 'The RIB’s fuel gauge — check it before trusting the escape route.', essential: false, act: 'Act Three' }, + ], cast: [], props: [], }; diff --git a/src/scenarios/princes-bride.js b/src/scenarios/princes-bride.js index 26ba8f8..24c5c33 100644 --- a/src/scenarios/princes-bride.js +++ b/src/scenarios/princes-bride.js @@ -13,7 +13,22 @@ export default { { id: 'act3', label: 'Act Three done — verdict + bargain answered', targetMin: 195 }, { id: 'epi', label: 'Epilogue + debrief done', targetMin: 210 }, ], - clues: [], + clues: [ + { id: 'c1', label: 'Trial is at dawn, Market Hall (Vane’s announcement — automatic).', essential: true, act: 'Act One — The Gathering Storm' }, + { id: 'c2', label: 'Ashford drowned in debts long before any “curse” (Dallam / letters / Garratt’s notes).', essential: true, act: 'Act One — The Gathering Storm' }, + { id: 'c3', label: 'Garratt’s verdict column is blank — he is waiting to see what pays.', essential: true, act: 'Act One — The Gathering Storm' }, + { id: 'c4', label: 'McShay’s evidence stack: poppet, livestock, “seen at the stones”.', essential: true, act: 'Act One — The Gathering Storm' }, + { id: 'c5', label: 'Eleanor refused McShay’s proposal five years ago (Eleanor / William / letters).', essential: true, act: 'Act One — The Gathering Storm' }, + { id: 'c6', label: 'Meadowsweet on the cell sill — fresh, in October.', essential: false, act: 'Act One — The Gathering Storm' }, + { id: 'c7', label: '“Master Drago” knows Polish court etiquette rather well for a wool man.', essential: false, act: 'Act One — The Gathering Storm' }, + { id: 'c8', label: 'Poppet stitching = Mary Fletcher’s needlework.', essential: true, act: 'Act Two — The Deeper Dark' }, + { id: 'c9', label: 'Mary’s confession — mercy is the mechanic; protection before testimony.', essential: true, act: 'Act Two — The Deeper Dark' }, + { id: 'c10', label: 'Ashford debt letters + unsent apology note (kills the curse on paper).', essential: true, act: 'Act Two — The Deeper Dark' }, + { id: 'c11', label: 'McShay’s unsent letter to Eleanor — the lever: mercy silences, cruelty breaks.', essential: true, act: 'Act Two — The Deeper Dark' }, + { id: 'c12', label: 'Thorne’s journal — the courtesies, the Bright Lady, Margaret named years ago.', essential: false, act: 'Act Two — The Deeper Dark' }, + { id: 'c13', label: 'The Bright Lady’s claim & terms-space (the stones at dusk; else she attends the trial).', essential: true, act: 'Act Two — The Deeper Dark' }, + { id: 'c14', label: 'Garratt’s price: “Give me evidence I can cite, and I’ll happily be the man who saved an innocent.”', essential: false, act: 'Act Two — The Deeper Dark' }, + ], cast: [], props: [], }; diff --git a/src/scenarios/silvery-moon.js b/src/scenarios/silvery-moon.js index 1e99189..2f80e4c 100644 --- a/src/scenarios/silvery-moon.js +++ b/src/scenarios/silvery-moon.js @@ -13,7 +13,16 @@ export default { { id: 'rite', label: 'The rite answered — Crowe’s fate decided', targetMin: 190 }, { id: 'epi', label: 'Epilogue — totals, photographs, the last Listen roll', targetMin: 210 }, ], - clues: [], + clues: [ + { id: 'c1', label: 'Braddock wears their silver ring', essential: true, fallback: 'Mary saw it; Danny’s midnight crate' }, + { id: 'c2', label: 'The 1861 collapse was a rite (“they were SINGING down there”)', essential: true, fallback: 'Whiskey Tom, three whiskies in' }, + { id: 'c3', label: 'Chicago is the money end — the trampled map-case', essential: true, fallback: 'the pendant’s Chicago maker’s-mark' }, + { id: 'c4', label: '“They do not summon it. They ANSWER it.” — the tome page', essential: true }, + { id: 'c5', label: 'Silverstrike + the date + 10:47 (Lily / manifest / thrall)', essential: true, fallback: 'any one of three sources' }, + { id: 'c6', label: 'Lot 47 — six by two by two, with air holes', essential: true, fallback: 'the thrall says it plainly' }, + { id: 'c7', label: 'THE ALMANAC — new moon on the 21st; no October eclipse; Nov 4’s real one invisible in America', essential: true }, + { id: 'c8', label: 'The False Moon’s first rise — a full moon in a sky that has none (SAN 0/1)', essential: true }, + ], cast: [], props: [], }; diff --git a/src/scenarios/vain-crown.js b/src/scenarios/vain-crown.js index 257b96d..cc6b7c9 100644 --- a/src/scenarios/vain-crown.js +++ b/src/scenarios/vain-crown.js @@ -13,7 +13,14 @@ export default { { id: 'crown', label: 'THE CROWN CHOICE by 3:15 — card face-down, stop narrating', targetMin: 195, hardTrigger: true }, { id: 'epi', label: 'Epilogue done — Glory aloud, best vanity toasted', targetMin: 210 }, ], - clues: [], + clues: [ + { id: 'c1', label: 'The two-key door foretold (“a scholar’s and a thief’s”)', essential: true, fallback: 'Yrsa reads it on the door itself' }, + { id: 'c2', label: 'Vesper rode at dawn by the forest road', essential: true, fallback: 'Marrow saw it from her window' }, + { id: 'c3', label: 'The open-water road is watched', essential: true, fallback: 'the wolves are simply there if taken — the warning was mercy, not a gate' }, + { id: 'c4', label: 'The pilgrim is a watcher', essential: true, fallback: 'he bolts at 1:00 regardless' }, + { id: 'c5', label: 'The crown’s bargain: +1 Charm forever · Vice: Pride · a standing Bane no humility clears', essential: true }, + { id: 'c6', label: 'Cassian’s plea lands: “vain, not evil — it matters” (shapes the reburial ending)', essential: true }, + ], cast: [], props: [], }; diff --git a/tests/scenarios/clues.test.js b/tests/scenarios/clues.test.js new file mode 100644 index 0000000..3699e29 --- /dev/null +++ b/tests/scenarios/clues.test.js @@ -0,0 +1,46 @@ +import { describe, it, expect } from 'vitest'; +import continuum from '../../src/con/continuum-2026.js'; +import { getScenario } from '../../src/scenarios/index.js'; +import { analyzeClues } from '../../src/clues/safety-net.js'; + +const scenarios = continuum.slots.map((s) => getScenario(s.scenarioId)); + +describe('ported clue trails', () => { + it('every Continuum scenario has a non-empty clue trail', () => { + for (const sc of scenarios) { + expect(sc.clues.length, sc.meta.id).toBeGreaterThan(0); + } + }); + + it('clues are well-formed with unique ids and at least one essential', () => { + for (const sc of scenarios) { + const ids = sc.clues.map((c) => c.id); + expect(new Set(ids).size, sc.meta.id + ' unique ids').toBe(ids.length); + for (const c of sc.clues) { + expect(typeof c.label, sc.meta.id).toBe('string'); + expect(c.label.length, sc.meta.id).toBeGreaterThan(0); + expect(typeof c.essential, sc.meta.id).toBe('boolean'); + } + expect(sc.clues.some((c) => c.essential), sc.meta.id + ' has essentials').toBe(true); + } + }); + + it('the safety-net reads each trail: unsolved cold, solvable once essentials revealed', () => { + for (const sc of scenarios) { + const cold = analyzeClues(sc.clues, []); + expect(cold.solvable, sc.meta.id).toBe(false); + const essentialIds = sc.clues.filter((c) => c.essential).map((c) => c.id); + const warm = analyzeClues(sc.clues, essentialIds); + expect(warm.solvable, sc.meta.id).toBe(true); + expect(warm.missingEssential, sc.meta.id).toEqual([]); + } + }); + + it('missing essentials still carry their fallback where authored', () => { + // day-one c2 is an essential clue with a fallback. + const dayOne = getScenario('day-one'); + const r = analyzeClues(dayOne.clues, []); + const c2 = r.missingEssential.find((c) => c.id === 'c2'); + expect(c2.fallback).toContain('radio crackles'); + }); +});