feat: port all six Continuum 2026 slots + real schedule times

Rewrite continuum-2026.js from the authoritative con-desk.html SLOTS
array: six real slots with days/times/systems/players and startsAt set,
so the hub's live-now/up-next follows the real convention clock.

Add five scenario modules (day-one, vain-crown, silvery-moon, chopper,
princes-bride) transcribed from the existing gm-utility console
timelines, and register them so every slot is tappable. Day One (BRP)
borrows the coc-d100 pack; the others map to their own systems.

Corrections found while porting: the slate is six slots (Slot 5 Silvery
Moon was missing) and s6 is 'Get to the Chopper — Another Fine Mess'.

tests/con/continuum-2026.test.js guards slot resolvability, valid dice
packs, and schedule status. 125 tests pass; build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
slaguru666
2026-07-20 21:52:53 +01:00
co-authored by Claude Opus 4.8
parent b57880d630
commit b4d797b52d
10 changed files with 167 additions and 22 deletions
+3 -3
View File
@@ -65,7 +65,7 @@ Components are dumb renderers that emit events; the shell wires them together.
| `src/components/dice-tray.js` | `<dice-tray>` — 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` | `<gm-shell>` — 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
@@ -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).
+12 -11
View File
@@ -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' },
],
};
+19
View File
@@ -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: [],
};
+20
View File
@@ -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: [],
};
+8 -4
View File
@@ -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;
+19
View File
@@ -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: [],
};
+19
View File
@@ -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: [],
};
+19
View File
@@ -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: [],
};
+35
View File
@@ -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');
});
});