diff --git a/README.md b/README.md index fcbf095..fa439a1 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ Components are dumb renderers that emit events; the shell wires them together. | `src/components/dice-tray.js` | `` — pack selector + die picker + roll + verdict | | `src/components/npc-tray.js` · `art-tray.js` · `prop-viewer.js` · `clue-net.js` | The tray tools | | `src/components/gm-shell.js` | `` — root wiring + persistence | -| `src/scenarios/` | Scenario data files (`afterimage.js`; `example-with-clues.js` is a template) | +| `src/scenarios/` | Scenario data files (six Continuum slots: `afterimage`, `day-one`, `vain-crown`, `silvery-moon`, `chopper`, `princes-bride`; `example-with-clues.js` is a template); `index.js` is the registry | | `public/` | `manifest.webmanifest`, `sw.js` (offline service worker) | ## Scenario data model @@ -96,8 +96,8 @@ compress if you reach a beat behind schedule. 2. **Slice 2** — NPC generator (offline genre tables) + art generator (offline pencil-art library). ✅ 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). ✅ -6. **Later** — remaining tray tools (break timer, parking-lot note, wake-lock); online art "Generate"; scenario porting from the Continuum docs; real slot start-times for the hub; markdown → scenario-data generator; native iPad wrapper. +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. ## Docs diff --git a/docs/superpowers/plans/2026-07-20-director-slice5.md b/docs/superpowers/plans/2026-07-20-director-slice5.md index f07aea8..0e5ba88 100644 --- a/docs/superpowers/plans/2026-07-20-director-slice5.md +++ b/docs/superpowers/plans/2026-07-20-director-slice5.md @@ -42,8 +42,17 @@ jsdom. No new dependencies. Fully offline. - `npm test` → 122 passing (34 files); build succeeds (35 modules). Manual: hub renders the Continuum slate, "Open" deep-links AFTERIMAGE into a live Rail session, 🏠 returns to the hub. Console clean. -## Deferred (backlog, not this slice) +## Follow-up (completed after the slice) -- Real slot **start times** for Continuum 2026 (add each `startsAt` to light up live/up-next). -- Porting the four remaining scenarios (Day One, Vain Crown, Another Fine Mess, Prince's Bride) into - scenario modules + registering them (then flip each slot's `scenarioId`). +- **All six Continuum 2026 slots ported + scheduled.** `continuum-2026.js` rewritten from the + authoritative `con-desk.html` SLOTS array (real days/times/systems), every `startsAt` set so + live/up-next is live. Five new scenario modules (`day-one`, `vain-crown`, `silvery-moon`, + `chopper`, `princes-bride`) transcribe their timelines from the existing gm-utility consoles and + are registered; `tests/con/continuum-2026.test.js` guards resolvability + schedule. + - 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*. + +## Still deferred (backlog) + +- Porting each scenario's **clue trails / cast** into the data model (shipped `clues: []` like AFTERIMAGE). +- A dedicated **BRP** dice pack — Day One currently borrows `coc-d100` (d100 roll-under). diff --git a/src/con/continuum-2026.js b/src/con/continuum-2026.js index 355718e..5479804 100644 --- a/src/con/continuum-2026.js +++ b/src/con/continuum-2026.js @@ -1,16 +1,17 @@ -// Continuum 2026 — the real convention this build is first deployed for. -// Only AFTERIMAGE is ported into a scenario module today (scenarioId set); the -// other four slots are known slate metadata with scenarioId: null until they are -// ported (see the "scenario porting" backlog item). Slot start times are left -// null (TBC) rather than invented — add each slot's real `startsAt` (ISO local -// time) to light up the live / up-next timing on the hub. +// Continuum 2026 — Fri 24 – Sun 26 July 2026, High Leigh, Hoddesdon. +// Six slots, all ported. Schedule (days, times, systems, player counts) is +// transcribed from gm-utility/con-desk.html (the SLOTS array this hub succeeds). +// `slot` is the display label; `startsAt` (local ISO) + `playMinutes` (the slot +// window length) drive the live / up-next timing; `scenarioId` deep-links via +// the scenario registry. export default { meta: { id: 'continuum-2026', title: 'Continuum 2026' }, slots: [ - { id: 'c26-fri-1', scenarioId: null, title: 'Day One', slot: 'Fri · Slot 1', startsAt: null }, - { id: 'c26-fri-2', scenarioId: 'afterimage', title: 'AFTERIMAGE', slot: 'Fri · Slot 2', system: 'year-zero', players: 4, playMinutes: 210, startsAt: null }, - { id: 'c26-sat-4', scenarioId: null, title: 'Vain Crown', slot: 'Sat · Slot 4', startsAt: null }, - { id: 'c26-sun-6', scenarioId: null, title: 'Another Fine Mess', slot: 'Sun · Slot 6', system: 'coc-d100', startsAt: null }, - { id: 'c26-sun-7', scenarioId: null, title: "Prince's Bride", slot: 'Sun · Slot 7', system: 'dee-sanction', startsAt: null }, + { id: 'c26-s1', scenarioId: 'day-one', title: 'DAY ONE — London Falls', slot: 'Fri 24 · 14:00–18:00', system: 'BRP', players: '5–6', playMinutes: 240, startsAt: '2026-07-24T14:00:00' }, + { id: 'c26-s2', scenarioId: 'afterimage', title: 'AFTERIMAGE', slot: 'Fri 24 · 20:00–24:00', system: 'Blade Runner', players: 4, playMinutes: 240, startsAt: '2026-07-24T20:00:00' }, + { id: 'c26-s4', scenarioId: 'vain-crown', title: 'THE VAIN CROWN', slot: 'Sat 25 · 14:00–18:00', system: 'VANITY', players: 5, playMinutes: 240, startsAt: '2026-07-25T14:00:00' }, + { id: 'c26-s5', scenarioId: 'silvery-moon', title: 'BY THE LIGHT OF THE SILVERY MOON', slot: 'Sat 25 · 20:00–24:00', system: 'CoC Gaslight', players: 6, playMinutes: 240, startsAt: '2026-07-25T20:00:00' }, + { id: 'c26-s6', scenarioId: 'chopper', title: 'GET TO THE CHOPPER — Another Fine Mess', slot: 'Sun 26 · 10:00–13:00', system: 'Pulp Cthulhu', players: 6, playMinutes: 180, startsAt: '2026-07-26T10:00:00' }, + { id: 'c26-s7', scenarioId: 'princes-bride', title: 'THE PRINCES BRIDE', slot: 'Sun 26 · 14:00–18:00', system: 'Dee Sanction', players: '4–6', playMinutes: 240, startsAt: '2026-07-26T14:00:00' }, ], }; diff --git a/src/scenarios/chopper.js b/src/scenarios/chopper.js new file mode 100644 index 0000000..fe02ef2 --- /dev/null +++ b/src/scenarios/chopper.js @@ -0,0 +1,19 @@ +// GET TO THE CHOPPER — Another Fine Mess · Continuum 2026 · Sun Slot 6. +// Pulp Cthulhu (~2h50 play). Timeline transcribed from +// gm-utility/chopper-console.html (SCHED array). Core rolls are d100; the +// house Panic & Glory pack is available in the dice tray for the gear-change. +export default { + meta: { id: 'chopper', title: 'GET TO THE CHOPPER — Another Fine Mess', system: 'coc-d100', players: 6, playMinutes: 170, slot: 'Sun · Slot 6' }, + timeline: [ + { id: 'brief', label: 'Briefing done — mission clear, first one-liner attempted', targetMin: 10 }, + { id: 'act1', label: 'Act One done — ashore; agents at the Palacio, machine on the volcano, test at one', targetMin: 50 }, + { id: 'act2', label: 'Act Two done — Willis freed, Segal handled, vault empty: plans already up the mountain', targetMin: 110 }, + { id: 'hard', label: 'HARD CUT 2:15 — the SIREN begins its power-up regardless; go to the countdown', targetMin: 135, hardTrigger: true }, + { id: 'act3', label: 'Act Three resolved — Volkova turned / array re-tuned / C4 — the volcano has had enough', targetMin: 145 }, + { 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: [], + cast: [], + props: [], +}; diff --git a/src/scenarios/day-one.js b/src/scenarios/day-one.js new file mode 100644 index 0000000..7ec9e35 --- /dev/null +++ b/src/scenarios/day-one.js @@ -0,0 +1,20 @@ +// DAY ONE — "London Falls" · Continuum 2026 · Fri Slot 1. +// Timeline transcribed from gm-utility/day-one-console.html (SCHED array). +// System is BRP; mapped to the d100 roller pack (no dedicated BRP pack yet). +export default { + meta: { id: 'day-one', title: 'DAY ONE — London Falls', system: 'coc-d100', players: 6, playMinutes: 210, slot: 'Fri · Slot 1' }, + timeline: [ + { id: 'intro', label: 'Intro done — six placed, alert fired, players converging', targetMin: 15 }, + { id: 'market', label: 'MARKET BREAKS — vendor bitten in full view, stampede (force it now if not yet)', targetMin: 60, hardTrigger: true }, + { id: 'act1', label: 'Act One done — first infected met, group formed', targetMin: 75 }, + { id: 'cordon', label: 'CORDON FRAGMENT — "hold the north bank line — do not cross" (force by now)', targetMin: 150, hardTrigger: true }, + { id: 'act2', label: 'Act Two done — pub resolved, reanimation witnessed, route chosen', targetMin: 165 }, + { id: 'cost', label: 'THE COST — someone does not make it. Force it. Do not soften it', targetMin: 185, hardTrigger: true }, + { id: 'reach', label: 'Bridge or boat reached', targetMin: 190 }, + { id: 'act3', label: 'Act Three done — cost paid', targetMin: 195 }, + { id: 'epi', label: 'Epilogue + debrief done — tangent seeded', targetMin: 210 }, + ], + clues: [], + cast: [], + props: [], +}; diff --git a/src/scenarios/index.js b/src/scenarios/index.js index fadf44a..967a71e 100644 --- a/src/scenarios/index.js +++ b/src/scenarios/index.js @@ -1,12 +1,16 @@ // Scenario registry — maps a scenario id to its module so the con hub can // deep-link a slot into the shell. Register each new scenario module here. import afterimage from './afterimage.js'; +import dayOne from './day-one.js'; +import vainCrown from './vain-crown.js'; +import silveryMoon from './silvery-moon.js'; +import chopper from './chopper.js'; +import princesBride from './princes-bride.js'; import exampleWithClues from './example-with-clues.js'; -export const SCENARIOS = { - [afterimage.meta.id]: afterimage, - [exampleWithClues.meta.id]: exampleWithClues, -}; +const modules = [afterimage, dayOne, vainCrown, silveryMoon, chopper, princesBride, exampleWithClues]; + +export const SCENARIOS = Object.fromEntries(modules.map((m) => [m.meta.id, m])); export function getScenario(id) { return SCENARIOS[id] || null; diff --git a/src/scenarios/princes-bride.js b/src/scenarios/princes-bride.js new file mode 100644 index 0000000..26ba8f8 --- /dev/null +++ b/src/scenarios/princes-bride.js @@ -0,0 +1,19 @@ +// THE PRINCES BRIDE · Continuum 2026 · Sun Slot 7 · The Dee Sanction. +// Timeline transcribed from gm-utility/princes-bride/modules/princes-bride.js +// (schedule array). The source marks no per-beat hard flags; the one authored +// hard cut is the Act Two bell at 2:45 (HARD 2:45 — the bell rings regardless). +export default { + meta: { id: 'princes-bride', title: 'THE PRINCES BRIDE', system: 'dee-sanction', players: 6, playMinutes: 210, slot: 'Sun · Slot 7' }, + timeline: [ + { id: 'intro', label: 'Intro done — mission understood, McShay witnessed', targetMin: 15 }, + { id: 'act1', label: 'Act One done — trial-at-dawn announced; BREAK', targetMin: 75 }, + { id: 'mary', label: 'Mary Fletcher confession secured (or route known)', targetMin: 120 }, + { id: 'dusk', label: 'Dusk at the stones — the Bright Lady’s overture', targetMin: 140 }, + { id: 'act2', label: 'Act Two done — evidence loaded (HARD CUT at 2:45: the bell)', targetMin: 165, hardTrigger: true }, + { id: 'act3', label: 'Act Three done — verdict + bargain answered', targetMin: 195 }, + { id: 'epi', label: 'Epilogue + debrief done', targetMin: 210 }, + ], + clues: [], + cast: [], + props: [], +}; diff --git a/src/scenarios/silvery-moon.js b/src/scenarios/silvery-moon.js new file mode 100644 index 0000000..1e99189 --- /dev/null +++ b/src/scenarios/silvery-moon.js @@ -0,0 +1,19 @@ +// BY THE LIGHT OF THE SILVERY MOON · Continuum 2026 · Sat Slot 5. +// Call of Cthulhu — Gaslight 1892. Timeline transcribed from +// gm-utility/silvery-moon-console.html (SCHED array). +export default { + meta: { id: 'silvery-moon', title: 'BY THE LIGHT OF THE SILVERY MOON', system: 'coc-d100', players: 6, playMinutes: 210, slot: 'Sat · Slot 5' }, + timeline: [ + { id: 'intro', label: 'Intro done — telegrams dealt, party at Fletcher’s', targetMin: 15 }, + { id: 'act1', label: 'LEAVE STILLWATER by 1:05 — unfought Shambler becomes the station scene', targetMin: 65, hardTrigger: true }, + { id: 'act2a', label: 'Chicago: Dunworthy named, Lily date heard', targetMin: 95 }, + { id: 'act2', label: 'LEAVE CHICAGO by 2:00 — the lake rite is the exit, not a dungeon', targetMin: 120, hardTrigger: true }, + { id: 'inter', label: 'Interlude done — the almanac landed; the False Moon first seen', targetMin: 130 }, + { id: 'mine', label: 'INTO THE CHAMBER by 2:50 — the eclipse will not wait', targetMin: 170, hardTrigger: true }, + { 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: [], + cast: [], + props: [], +}; diff --git a/src/scenarios/vain-crown.js b/src/scenarios/vain-crown.js new file mode 100644 index 0000000..257b96d --- /dev/null +++ b/src/scenarios/vain-crown.js @@ -0,0 +1,19 @@ +// THE VAIN CROWN · Continuum 2026 · Sat Slot 4 · VANITY (house d6-pool system). +// Timeline transcribed from gm-utility/vain-crown-console.html (SCHED array). +export default { + meta: { id: 'vain-crown', title: 'THE VAIN CROWN', system: 'vanity-d6', players: 5, playMinutes: 210, slot: 'Sat · Slot 4' }, + timeline: [ + { id: 'intro', label: 'Intro done — pregens dealt, dice primer run, pitch read', targetMin: 15 }, + { id: 'act1', label: 'THE WHISTLE — Pale Stranger bolts whatever is happening; Act Two begins', targetMin: 60, hardTrigger: true }, + { id: 'act2', label: 'Act Two done — green won, prisoner or road named', targetMin: 100 }, + { id: 'act3', label: 'BARROW MOUTH reached — whatever is uncaught stays behind', targetMin: 135, hardTrigger: true }, + { id: 'door', label: 'The Sealed Door opened — both keys turned', targetMin: 150 }, + { id: 'crypt', label: 'Crypt crossed — Turn Undead staged big, Bram held the choke', targetMin: 170 }, + { id: 'throne', label: 'THRONE FIGHT starts by 2:50 — Vesper parleys when it turns', targetMin: 170, hardTrigger: true }, + { 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: [], + cast: [], + props: [], +}; diff --git a/tests/con/continuum-2026.test.js b/tests/con/continuum-2026.test.js new file mode 100644 index 0000000..99e9ef5 --- /dev/null +++ b/tests/con/continuum-2026.test.js @@ -0,0 +1,35 @@ +import { describe, it, expect } from 'vitest'; +import continuum from '../../src/con/continuum-2026.js'; +import { getScenario } from '../../src/scenarios/index.js'; +import { validateScenario } from '../../src/core/scenario.js'; +import { getRulePack } from '../../src/dice/rulepacks/index.js'; +import { analyzeSchedule } from '../../src/con/schedule.js'; + +describe('Continuum 2026 con', () => { + it('lists the six ported slots', () => { + expect(continuum.slots).toHaveLength(6); + expect(continuum.slots.every((s) => s.scenarioId)).toBe(true); + }); + + it('every slot deep-links to a valid scenario with a known dice pack', () => { + for (const slot of continuum.slots) { + const scenario = getScenario(slot.scenarioId); + expect(scenario, slot.scenarioId).not.toBe(null); + expect(validateScenario(scenario), slot.scenarioId).toEqual([]); + expect(getRulePack(scenario.meta.system), scenario.meta.system).not.toBe(null); + } + }); + + it('marks the right slot live / next during the convention', () => { + // Sat 25 July, 15:00 local — the Vain Crown slot (14:00–18:00) is running. + // Parsing now with the same local (no-Z) format as the slots keeps this + // assertion timezone-independent. + const r = analyzeSchedule(continuum.slots, Date.parse('2026-07-25T15:00:00')); + expect(r.liveNow?.scenarioId).toBe('vain-crown'); + expect(r.upNext?.scenarioId).toBe('silvery-moon'); + const byId = Object.fromEntries(r.slots.map((s) => [s.scenarioId, s.status])); + expect(byId['day-one']).toBe('done'); + expect(byId.afterimage).toBe('done'); + expect(byId.chopper).toBe('upcoming'); + }); +});