feat: port all six scenarios' clue trails into the data model
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b4d797b52d
commit
ce91b4ce98
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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: [],
|
||||
};
|
||||
|
||||
@@ -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: [],
|
||||
};
|
||||
|
||||
@@ -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: [],
|
||||
};
|
||||
|
||||
@@ -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: [],
|
||||
};
|
||||
|
||||
@@ -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: [],
|
||||
};
|
||||
|
||||
@@ -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: [],
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user