From 4096291b1d2ab7fd71e674333cb3cd15d8d3ac0d Mon Sep 17 00:00:00 2001 From: slaguru666 <111923774+slaguru666@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:18:52 +0100 Subject: [PATCH] feat: port all six scenarios' NPC cast rosters into the data model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Populate cast[] (kind:'npc') for every Continuum scenario, transcribed from the gm-utility NPC sources — normalising the two schemas (console {id,name,ess,sec} and module {name,tag,secret}) into { id, name, kind, note, secret? }. 57 NPCs total. Pregens/PCs excluded (separate character-sheet system). No cast UI consumer yet — the design reserves scenario.cast for a future cast dashboard, so this is a data-model port guarded by tests/scenarios/cast.test.js (shape + unique ids). 131 tests pass. Co-Authored-By: Claude Opus 4.8 --- README.md | 2 +- .../plans/2026-07-20-director-slice5.md | 8 +++++- src/scenarios/afterimage.js | 12 ++++++++- src/scenarios/chopper.js | 11 +++++++- src/scenarios/day-one.js | 10 ++++++- src/scenarios/princes-bride.js | 15 ++++++++++- src/scenarios/silvery-moon.js | 15 ++++++++++- src/scenarios/vain-crown.js | 12 ++++++++- tests/scenarios/cast.test.js | 27 +++++++++++++++++++ 9 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 tests/scenarios/cast.test.js diff --git a/README.md b/README.md index c824e57..0090519 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' 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. +6. **Later** — remaining tray tools (break timer, parking-lot note, wake-lock); online art "Generate"; a cast-dashboard UI to surface the ported `scenario.cast` rosters; 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 91ef684..0b5b8a9 100644 --- a/docs/superpowers/plans/2026-07-20-director-slice5.md +++ b/docs/superpowers/plans/2026-07-20-director-slice5.md @@ -59,7 +59,13 @@ jsdom. No new dependencies. Fully offline. clue `essential: true` (their source is a single curated spine); day-one and princes-bride carry the source's own essential/optional split. +- **All six scenarios' cast rosters ported** (NPCs, `kind: 'npc'`) via `extract-cast.mjs`, normalising + the two source schemas (`{id,name,ess,sec}` consoles and `{name,tag,secret}` modules) to + `{ id, name, kind, note, secret? }`. `tests/scenarios/cast.test.js` guards shape + unique ids. + Note: the `cast` field has no UI consumer yet (the design reserves it for a future cast dashboard) — + this is a data-model port. Pregens/PCs are intentionally excluded (separate character-sheet/handout system). + ## Still deferred (backlog) -- Porting each scenario's **cast** into the data model (still `cast: []`). +- A **cast dashboard** UI to surface `scenario.cast` (parallels ``). - 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 c2cf384..0a193e0 100644 --- a/src/scenarios/afterimage.js +++ b/src/scenarios/afterimage.js @@ -16,6 +16,16 @@ export default { { 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: [], + cast: [ + { id: 'cmdr-bryant', name: 'Cmdr. Bryant', kind: 'npc', note: 'RDU commander — ally, guarded', secret: 'If asked to expedite the IDENT CONFLICT desk review: “Can’t. Wallace liaison put a hold on all RDU record reviews tonight. Funny, that.”' }, + { id: 'dr-avis', name: 'Dr. Avis', kind: 'npc', note: 'Coroner — friendly' }, + { id: 'wren-unit-a-1', name: 'Wren (unit A-1)', kind: 'npc', note: 'The runner — flight, then fragile trust', secret: 'If she escapes: origami sparrow under the wiper — YOU WANT THE TRUTH. THE PIER. DAWN.' }, + { id: 'lark-unit-a-2', name: 'Lark (unit A-2)', kind: 'npc', note: 'The still one — cooperative, composed', secret: 'Under the grief: relief at being found out, without knowing for what.' }, + { id: 'aris-kade', name: 'Aris Kade', kind: 'npc', note: 'The original — 38, exhausted beyond fear', secret: 'Truth-ending lever: “Say something. One of you. They’re your witnesses too.”' }, + { id: 'corvin', name: 'Corvin', kind: 'npc', note: 'Wallace Asset Protection — a deadline in a good coat', secret: 'His warrant covers units and media ONLY — no paper on a human. A human homicide collapses his pretext. He will trade. Even his defeats are tidy.' }, + { id: 'ap-officers-2', name: 'AP Officers ×2', kind: 'npc', note: 'Follow the warrant, not the man' }, + { id: 'street-extras-improv', name: 'Street extras (improv)', kind: 'npc', note: 'Off-script encounters', secret: 'Fortune-teller line, aimed at a replicant PC: “Two birds with one song. The cage is open, officer. Whose hand closed it?”' }, + { id: 'auntie-bo', name: 'Auntie Bo', kind: 'npc', note: 'Night-market broth queen — stall 44', secret: 'Will never sell Wren out — a phone call ahead only means Wren is already spooked.' }, + ], props: [], }; diff --git a/src/scenarios/chopper.js b/src/scenarios/chopper.js index 2263c06..562c994 100644 --- a/src/scenarios/chopper.js +++ b/src/scenarios/chopper.js @@ -22,6 +22,15 @@ export default { { 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: [], + cast: [ + { id: 'esc', name: 'COMANDANTE ESCORPIÓN', kind: 'npc', note: 'Strongman, half-transformed; mirror shades over nictitating membranes' }, + { id: 'volkova', name: 'COL. KATYA VOLKOVA', kind: 'npc', note: 'Soviet handler — the hinge, not the villain. Carries the launch keys', secret: 'From the turn she counts as a pulp ally and shoots like one. Superb chopper-door face for the final frame.' }, + { id: 'padre', name: 'PADRE MARISCO', kind: 'npc', note: 'Salt-cured priest of the Trench; conducts rather than fights' }, + { id: 'singers', name: 'STONE-SINGERS', kind: 'npc', note: 'The note does not stop for breath — or bullets, entirely' }, + { id: 'willis', name: 'AGENT B. WILLIS', kind: 'npc', note: 'Ally from the cells — barefoot, vest, mouth running' }, + { id: 'segal', name: 'AGENT S. SEGAL', kind: 'npc', note: 'Silk robe, herbal tea, whale-song — hypno-conditioned, not a traitor', secret: 'Never fixed? He wanders toward the volcano "for the ceremony" at the worst moment. Comedy becomes stakes.' }, + { id: 'mooks', name: 'RED TIDE MOOKS', kind: 'npc', note: 'They miss, gloriously. They drop from one hit. That is their whole job' }, + { id: 'novia', name: 'LA NOVIA — THE BRIDE', kind: 'npc', note: 'Never statted. Some things are endings, not encounters' }, + ], props: [], }; diff --git a/src/scenarios/day-one.js b/src/scenarios/day-one.js index 215c1c8..b41b717 100644 --- a/src/scenarios/day-one.js +++ b/src/scenarios/day-one.js @@ -28,6 +28,14 @@ export default { { 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: [], + cast: [ + { id: 'fatima', name: 'FATIMA BALOGUN', kind: 'npc', note: 'Market stall owner — first competent adult voice' }, + { id: 'callum', name: 'PC CALLUM FRASER', kind: 'npc', note: 'Met constable, 28 — institution failing around him', secret: 'Heard "first reports… Elephant… 07:14" before comms went patchy. Hasn’t told anyone. When the cordon fragment lands, play his face first.' }, + { id: 'sharif', name: 'DR YASMIN SHARIF', kind: 'npc', note: 'Off-duty A&E consultant — will name what this is', secret: 'She wants to reach St Thomas’ — 2.5 km west. She is wrong. The state of Guy’s (visible from the car park) is the counter-argument. Losing her costs the group its best medic.' }, + { id: 'marcus', name: 'MARCUS', kind: 'npc', note: '19, livestreaming to 41,000 — walking liability', secret: 'Phone at 31%. When it dies, he’s just a scared 19-year-old — that’s a beat, not a punishment. Chat reports military massing at London Bridge north approach.' }, + { id: 'helen', name: 'HELEN', kind: 'npc', note: '"I’m sure it’s a gas leak." The bravest person here', secret: 'Thirty years Cabinet Office. "There’s a protocol for this. If they haven’t activated it, something went wrong before the alert." Names Meridian if trusted. THE MOMENT: once, she steps into danger for someone else. Play it straight. Recommended reanimation victim.' }, + { id: 'pete', name: 'PETE', kind: 'npc', note: 'Bridge Group foreman — triage with no good options', secret: 'Knows the cordon is real (someone in his group has a radio). Hasn’t told his people. Has already turned away two infected and dealt with the consequences — he is broken, not cruel.' }, + { id: 'minors', name: 'MINORS — Gordon · Danny · Declan & Ryan · Bridge Group', kind: 'npc', note: 'Stat as minors', secret: 'DANNY: bitten 40 min before the pub; jacket over the arm; turns if the group stays ~30 min — Sharif spots him in 5. RYAN is also bitten (blood on sleeve) — post-alert bites already symptomatic.' }, + ], props: [], }; diff --git a/src/scenarios/princes-bride.js b/src/scenarios/princes-bride.js index 24c5c33..ede72ce 100644 --- a/src/scenarios/princes-bride.js +++ b/src/scenarios/princes-bride.js @@ -29,6 +29,19 @@ export default { { 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: [], + cast: [ + { id: 'margaret-patterson', name: 'Margaret Patterson', kind: 'npc', note: 'the accused — innocent of the charge; woven with the uncanny', secret: 'Meadowsweet follows her. The Bright Lady calls her “little candle”.' }, + { id: 'father-murphy-mcshay', name: 'Father Murphy McShay', kind: 'npc', note: 'a wound preaching' }, + { id: 'witch-finder-garratt', name: 'Witch Finder Garratt', kind: 'npc', note: 'a professional waiting to see what pays' }, + { id: 'mary-fletcher', name: 'Mary Fletcher', kind: 'npc', note: 'grief stitched into spite, waiting to be caught', secret: 'Fallback: she breaks down publicly at dusk prayers.' }, + { id: 'count-drago-karnkowski', name: 'Count Drago Karnkowski', kind: 'npc', note: 'brave, loyal, out of his depth' }, + { id: 'thomas-patterson', name: 'Thomas Patterson', kind: 'npc', note: 'a father bargaining with fate' }, + { id: 'eleanor-patterson', name: 'Eleanor Patterson', kind: 'npc', note: 'a mother afraid of her own daughter' }, + { id: 'alderman-vane', name: 'Alderman Vane', kind: 'npc', note: 'order first, mercy where affordable' }, + { id: 'william-mcshay', name: 'William McShay', kind: 'npc', note: 'a boy drowning in his uncle’s certainty' }, + { id: 'roger-dallam', name: 'Roger Dallam', kind: 'npc', note: 'knows everything, risks nothing' }, + { id: 'the-bright-lady', name: 'The Bright Lady', kind: 'npc', note: 'not evil — alien, proprietary, patient', secret: 'First sight: Unravelling roll, every witness. No roll wins the parley. If no bargain: she curtseys, withdraws, and the claim stays OPEN.' }, + { id: 'agatha-thorne-d-2-yrs', name: 'Agatha Thorne (d. 2 yrs)', kind: 'npc', note: 'the last keeper of the courtesies' }, + ], props: [], }; diff --git a/src/scenarios/silvery-moon.js b/src/scenarios/silvery-moon.js index 2f80e4c..4b31fbc 100644 --- a/src/scenarios/silvery-moon.js +++ b/src/scenarios/silvery-moon.js @@ -23,6 +23,19 @@ export default { { 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: [], + cast: [ + { id: 'mary', name: 'MARY FLETCHER', kind: 'npc', note: 'Innkeeper; saw the watchers; knows the sheriff’s ring', secret: 'She sewed nothing — but she saw Crowe sew: the pendant in the mattress is findable because Mary remembers him doing it.' }, + { id: 'braddock', name: 'SHERIFF BRADDOCK', kind: 'npc', note: 'The law, in the cult’s ring — coerced, ashamed', secret: 'HE WIRES CHICAGO TONIGHT regardless. If the line is cut — the wire already went, or a rider goes. The countdown never depends on the players failing.' }, + { id: 'danny', name: 'DANNY TATE', kind: 'npc', note: 'Stable boy, 14; saw the midnight taking' }, + { id: 'tom', name: 'WHISKEY TOM GRAVES', kind: 'npc', note: 'Last old miner; the 1861 collapse, once removed' }, + { id: 'shambler', name: 'THE MINE GUARDIAN', kind: 'npc', note: 'Dimensional Shambler — abducts, not kills' }, + { id: 'enforcers', name: 'CULT ENFORCERS', kind: 'npc', note: 'Dark coats, silver rings, capture-first' }, + { id: 'thrall', name: 'THE CAPTURED THRALL', kind: 'npc', note: 'Branded, broken, grateful — confirms everything' }, + { id: 'byakhee', name: 'BYAKHEE', kind: 'npc', note: 'Answers the broken lake-rite out of the fog' }, + { id: 'dole', name: 'EPHRAIM DOLE', kind: 'npc', note: 'Survivor’s son; dynamite, the back adit, will not enter' }, + { id: 'dunworthy', name: 'SILAS DUNWORTHY', kind: 'npc', note: 'Industrialist, believer, intended vessel — occupied at the last', secret: 'THE FINAL FORM (art exists — show it): at the apex the Aspect arrives AS him. His eternity, delivered as occupancy. SAN 1/1D8. Cap: no investigator dies without a warned, failed roll.' }, + { id: 'crowe', name: 'DR. EZEKIEL CROWE', kind: 'npc', note: 'The client — alive, on the altar, the best-informed man in the room' }, + { id: 'aspect', name: 'THE FALSE MOON / LUNAR ASPECT', kind: 'npc', note: 'Nyarlathotep as the light that watches', secret: 'When the rite breaks: the crossing shadow COMPLETES, the light goes out like a struck lamp, and the true moonless sky returns. Give it silence.' }, + ], props: [], }; diff --git a/src/scenarios/vain-crown.js b/src/scenarios/vain-crown.js index cc6b7c9..f412100 100644 --- a/src/scenarios/vain-crown.js +++ b/src/scenarios/vain-crown.js @@ -21,6 +21,16 @@ export default { { 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: [], + cast: [ + { id: 'reeve', name: 'REEVE PENNYWHISTLE', kind: 'npc', note: 'Velvet cap he can’t afford; wants it done quietly', secret: 'He hopes they bring the crown TO HIM. If anyone wears it before him — he kneels. First. Play it horrible.' }, + { id: 'marrow', name: 'GOODWIFE MARROW', kind: 'npc', note: 'The Drowned Duck; knows every rumour in three parishes', secret: 'She is the fallback for every missed Act One fact — she saw the dawn ride from her window.' }, + { id: 'cassian', name: 'BROTHER CASSIAN', kind: 'npc', note: 'Old, kind, quietly terrified — the conscience' }, + { id: 'stranger', name: 'THE PALE STRANGER', kind: 'npc', note: 'Vesper’s watcher in pilgrim’s clothes — too clean, too calm', secret: 'HARD TRIGGER: at 1:00 he bolts and blows the whistle whatever is happening. The act break never depends on the players.' }, + { id: 'robbers', name: 'GRAVE-ROBBERS ×4 (+wolf-handler)', kind: 'npc', note: 'Hired blades, owed money, scared of the pale one' }, + { id: 'wolves', name: 'DIRE WOLVES', kind: 'npc', note: 'Vesper’s sentry-beasts — one on the green, three in the wood' }, + { id: 'crypt', name: 'THE CRYPT — 6 SKELETONS + 2 GHOULS (+1 wanderer)', kind: 'npc', note: 'The king’s servants, still serving' }, + { id: 'vesper', name: 'VESPER KOUND', kind: 'npc', note: 'Scholar-turned-grave-robber; wants to be rich, then admired for it', secret: 'If he lives, he sends a polite, infuriating letter of thanks (Props tab). Read it in the epilogue.' }, + { id: 'aldric', name: 'KING ALDRIC — THE VAIN CORPSE', kind: 'npc', note: 'Nemesis. Wants only to be witnessed', secret: 'PHASE 2 AT GRIT 7 (telegraph a round ahead — "the crown begins to blaze"): Aura −1 die to all who ever failed it; he stops defending and attacks twice.' }, + ], props: [], }; diff --git a/tests/scenarios/cast.test.js b/tests/scenarios/cast.test.js new file mode 100644 index 0000000..f368e97 --- /dev/null +++ b/tests/scenarios/cast.test.js @@ -0,0 +1,27 @@ +import { describe, it, expect } from 'vitest'; +import continuum from '../../src/con/continuum-2026.js'; +import { getScenario } from '../../src/scenarios/index.js'; + +const scenarios = continuum.slots.map((s) => getScenario(s.scenarioId)); + +describe('ported cast rosters', () => { + it('every Continuum scenario has a non-empty cast', () => { + for (const sc of scenarios) { + expect(sc.cast.length, sc.meta.id).toBeGreaterThan(0); + } + }); + + it('cast entries are well-formed with unique ids and a valid kind', () => { + for (const sc of scenarios) { + const ids = sc.cast.map((c) => c.id); + expect(new Set(ids).size, sc.meta.id + ' unique ids').toBe(ids.length); + for (const c of sc.cast) { + expect(typeof c.id, sc.meta.id).toBe('string'); + expect(c.id.length, sc.meta.id).toBeGreaterThan(0); + expect(typeof c.name, sc.meta.id).toBe('string'); + expect(c.name.length, sc.meta.id).toBeGreaterThan(0); + expect(['pc', 'npc'], sc.meta.id + ' ' + c.id).toContain(c.kind); + } + } + }); +});