diff --git a/docs/scenarios/STANDING_ORDER.md b/docs/scenarios/STANDING_ORDER.md index 05da39d..b7218b4 100644 --- a/docs/scenarios/STANDING_ORDER.md +++ b/docs/scenarios/STANDING_ORDER.md @@ -10,7 +10,7 @@ |---|---| | **Version** | **v0.7**, 15 September 2026 — absorbs desk playtests 1–5; the slot is 3h15 | | **Can it be run?** | **Yes, from this document alone,** with the compendium for sheets and the three bestiary creatures. | -| **Is it convention-ready?** | **Nearly — what is left is a run with human beings.** Four desk passes, no human runs. `_1.md` (GM chair, 5291) · `_2.md` (Act Three stress, 8143, creatures run through the real fight harness) · `_3.md` (player chair, contrarian, 3307, reached ending 1) · `_4.md` (the clock, cuts taken as written, 6620). Every content defect the four passes found is fixed, and the budget question they raised is decided: **the slot is 3h15.** | +| **Is it convention-ready?** | **Nearly — what is left is a run with human beings.** Five desk passes, no human runs. `_1.md` (GM chair, 5291) · `_2.md` (Act Three stress, 8143, creatures run through the real fight harness) · `_3.md` (player chair, contrarian, 3307, reached ending 1) · `_4.md` (the clock, cuts taken as written, 6620) · `_5.md` (the retry fix, verified). Every content defect the five passes found is fixed, and the budget question they raised is decided: **the slot is 3h15.** | | **What it is** | A **three-act one-shot**. It ends in the session; there is no second part and the text must not promise one. | | **Players** | 4–6 from the duty roster. The cast is declared in Casting and the build enforces it. | | **Runtime** | **2h50 of play in three acts of 45 / 75 / 50 plus a ten-minute break — 3h00 wall, in a 3h15 slot.** Act Two is budgeted at its **uncut** base, which is the figure passes 1, 3 and 4 all reconcile to. The fifteen minutes of slack are deliberate. **The Pacing Note's cuts are not defaults; plan to take none of them** — and the budget above is the no-cuts budget, which is the pair v0.6 briefly got wrong. | @@ -36,10 +36,10 @@ ### Still to do -- **A desk playtest.** None has been run. The act timings, the Act Two interview load and - whether Act Three's third option is findable are all unverified. -- **Handout production.** The three handouts are written here as text and have no print - pack. CLEAN GROUND's `CLEAN_GROUND_HANDOUTS.html` is the model. +- **A run with human beings.** Five desk passes have been run; no human table has. +- **Handout production.** The three handouts are in the Foundry compendium (STANDING + ORDER — Player handouts) but have no print pack. CLEAN GROUND's + `CLEAN_GROUND_HANDOUTS.html` is the model. - **A decision on Kit Marlow** — see Open questions. She is the cost of the tidiest ending and the draft may be leaning on a seventeen-year-old too hard. @@ -427,7 +427,7 @@ table that thinks time is elastic will not believe the deadline. > it is four hundred metres of 1962 concrete and the lights still work. --- -## Act Two — Bellhouse *(~55 min)* +## Act Two — Bellhouse *(~75 min)* **Where:** the site. **What it is for:** the players meet a functioning civil service at the end of the world, and are shown the sum. @@ -990,8 +990,8 @@ Name these as *do not resolve* so a helpful GM does not answer them. - **Kit Marlow.** She is the cost of ending 1 and the draft may lean on a seventeen-year-old too hard. The alternative is to put the weight on the whole sixty-one and never give one of them a name — colder, and possibly better. -- **Runtime.** 45/55/50 is an estimate with nothing behind it. Act Two is the one most - likely to overrun: it is seven locations and six speaking parts. +- ✅ **Runtime — superseded.** The 45/55/50 estimate was replaced by the measured 45/75/50 + below. - **Is ending 3 findable?** It depends on the players reading clause 4 *and* clause 5 and thinking about scope rather than cancellation. It may need a nudge written into Pennyfeather. diff --git a/packs/standingorder/000003.log b/packs/standingorder/000003.log new file mode 100644 index 0000000..42fc31e Binary files /dev/null and b/packs/standingorder/000003.log differ diff --git a/packs/standingorder/CURRENT b/packs/standingorder/CURRENT new file mode 100644 index 0000000..1a84852 --- /dev/null +++ b/packs/standingorder/CURRENT @@ -0,0 +1 @@ +MANIFEST-000002 diff --git a/packs/standingorder/MANIFEST-000002 b/packs/standingorder/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/standingorder/MANIFEST-000002 differ diff --git a/system.json b/system.json index c04f7fa..d466a09 100644 --- a/system.json +++ b/system.json @@ -148,6 +148,13 @@ "type": "Adventure", "system": "ringbrp" }, + { + "name": "standingorder", + "label": "The Custodians · STANDING ORDER", + "path": "packs/standingorder", + "type": "Adventure", + "system": "ringbrp" + }, { "name": "prologue", "label": "The Custodians · OPEN DAY (prologue)", @@ -193,6 +200,7 @@ "packs": [ "throughtrain", "lastadmission", + "standingorder", "prologue", "starter", "expedition", diff --git a/tools/build-packs.mjs b/tools/build-packs.mjs index 552073e..e59ec38 100644 --- a/tools/build-packs.mjs +++ b/tools/build-packs.mjs @@ -1023,6 +1023,69 @@ function cullandScene() { + `${showJournal.pages.length} plates to show`); } +// STANDING ORDER — the three-act crossing case. Journals, plates and the section map; +// no cast actors, because nobody in it has a stat block. The GM pages are read from the +// scenario document itself at build time (see scenario-standingorder.mjs). Played on +// the duty-roster sheets in the Pregens pack. +{ + const SO = await import("./scenario-standingorder.mjs"); + + const showJournal = imageJournal("STANDING ORDER — Show the players", "so_show", SO.SO_PLATES); + const facesJournal = imageJournal("STANDING ORDER — The people", "so_faces", SO.SO_FACES); + const gmJournal = journalDoc("STANDING ORDER — Run the case", "so_gm", SO.SO_PAGES); + const handouts = journalDoc("STANDING ORDER — Player handouts", "so_handouts", SO.SO_HANDOUTS); + handouts.doc.ownership = { default: 2 }; + handouts.doc.flags = { ringbrp: { brand: true } }; + for (const pg of handouts.pages) pg.ownership = { default: 2 }; + + const sceneId = idFor("scene:bellhouse"); + const bellhouse = { + ...cullandScene(), + _id: sceneId, + name: "Bellhouse — section", + navName: "Bellhouse", + background: { src: "systems/ringbrp/art/maps/bellhouse-section.webp" }, + width: 2400, height: 1700, + initial: { x: 1200, y: 850, scale: 0.45 }, + grid: { type: 0, size: 100, style: "solidLines", thickness: 1, + color: "#6674d8", alpha: 0, distance: 5, units: "m" }, + flags: { ringbrp: { key: "bellhouse" } } + }; + + const advId = idFor("adventure:standing_order"); + const adventure = { + _id: advId, + name: "STANDING ORDER — The Bellhouse Seat", + img: IC("journal") ?? ICON.journal, + caption: "A three-act crossing case. A seat of government, and a war that never came.", + description: + "
Two hundred and six people went underground in 1974 to govern the country " + + "after the war. There was no war.
" + + "They have been governing it for fifty-two years, the order that put them there " + + "renews itself every ninety days, and the next renewal is on Thursday. The site's " + + "record now counts more people than the region it governs, and the record is about " + + "to make the region match.
" + + "Three acts, 3h00 of play in a 3h15 slot. Nobody below is a monster, and the last " + + "act is a decision rather than a fight. Played on the duty-roster sheets from the " + + "Pregens compendium.
", + actors: [], + items: [], + journal: [ + { ...gmJournal.doc, pages: gmJournal.pages }, + { ...handouts.doc, pages: handouts.pages }, + { ...showJournal.doc, pages: showJournal.pages }, + { ...facesJournal.doc, pages: facesJournal.pages } + ], + scenes: [bellhouse], combats: [], tables: [], macros: [], cards: [], playlists: [], folders: [], + sort: 0, ...BASE() + }; + + await writePack("standingorder", [[`!adventures!${advId}`, adventure]]); + console.log(` standing STANDING ORDER — ${SO.SO_PAGES.length} GM pages, ` + + `${SO.SO_HANDOUTS.length} handouts, ${showJournal.pages.length} plates, ` + + `${facesJournal.pages.length} faces`); +} + // OPEN DAY — the prologue. A much smaller adventure than BLACK PLATFORM: no case file, // no contact groups, no authority, no combat. The player characters are the ten // "(before)" pregens already in the Pregens pack — shipping a second copy here would diff --git a/tools/figures-baseline.json b/tools/figures-baseline.json index 0e447bc..cef183e 100644 --- a/tools/figures-baseline.json +++ b/tools/figures-baseline.json @@ -1,13 +1,13 @@ { "note": "Unmarked measurement figures in scenarios that use no citations. May fall, never rise. Generated by tools/check-figures.mjs --update.", "uncited": { - "tools/scenario-lastadmission.mjs": 1, - "tools/scenario-openday.mjs": 0, "tools/scenario-starter.mjs": 0, + "tools/scenario-openday.mjs": 0, + "tools/scenario-lastadmission.mjs": 1, "tools/scenario-throughtrain.mjs": 8, + "tools/scenario-standingorder.mjs": 0, + "docs/scenarios/THROUGH_TRAIN.md": 8, "docs/scenarios/LAST_ADMISSION.md": 1, - "docs/scenarios/OPEN_DAY.md": 0, - "docs/scenarios/STANDING_ORDER.md": 0, - "docs/scenarios/THROUGH_TRAIN.md": 8 + "docs/scenarios/OPEN_DAY.md": 0 } } diff --git a/tools/scenario-standingorder.mjs b/tools/scenario-standingorder.mjs new file mode 100644 index 0000000..db3ee7d --- /dev/null +++ b/tools/scenario-standingorder.mjs @@ -0,0 +1,179 @@ +/** + * STANDING ORDER — the three-act crossing case, as data. + * + * Unlike the other scenarios this one is not transcribed by hand. The GM pages are read + * out of docs/scenarios/STANDING_ORDER.md at build time, one page per top-level section, + * so the compendium cannot drift from the document the desk playtests were run against. + * The cost is a small Markdown reader below, which handles exactly the constructs the + * house format uses and throws on nothing — an unknown construct comes through as a + * paragraph, which is visible in the journal rather than silently dropped. + * + * No cast actors: nobody in this case has a stat block. The people below are not meant + * to be fought, and the three creatures are the bestiary's own. + */ +import { readFileSync } from "node:fs"; +import path from "node:path"; + +const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")), ".."); +const SOURCE = "docs/scenarios/STANDING_ORDER.md"; +const MD = readFileSync(path.join(ROOT, SOURCE), "utf8"); + +/* Authoring sections, not table material. */ +const SKIP = [/^STATUS\b/, /^Open questions\b/]; + +/* ---------------------------------------------------------------- markdown */ + +const esc = s => s.replace(/&/g, "&").replace(//g, ">"); + +function inline(s) { + return esc(s) + .replace(/`([^`]+)`/g, "$1")
+ .replace(/\*\*(.+?)\*\*/g, "$1")
+ .replace(/(^|[^*\w])\*(?!\s)(.+?)\*(?!\w)/g, "$1$2");
+}
+
+const cells = line => line.trim().replace(/^\||\|$/g, "").split("|").map(c => inline(c.trim()));
+
+function blocks(lines) {
+ const out = [];
+ let i = 0;
+ while (i < lines.length) {
+ const line = lines[i];
+ if (!line.trim() || /^---+\s*$/.test(line)) { i++; continue; }
+
+ let m;
+ if ((m = line.match(/^(#{3,6})\s+(.*)$/))) {
+ const n = Math.min(m[1].length - 1, 4);
+ out.push(`| ${c} | `).join("")}
${html}`); + continue; + } + if ((m = line.match(/^(\s*)([-*]|\d+\.)\s+/))) { + const ordered = /\d/.test(m[2]); + const items = []; + while (i < lines.length) { + const im = lines[i].match(/^(\s*)([-*]|\d+\.)\s+(.*)$/); + if (im && im[1].length === 0) { items.push([im[3]]); i++; continue; } + if (items.length && lines[i].trim() && /^\s+/.test(lines[i])) { + items.at(-1).push(lines[i].trim()); i++; continue; + } + break; + } + const tag = ordered ? "ol" : "ul"; + out.push(`<${tag}>${items.map(x => `
${inline(para.join(" "))}
`); + } + return out.join(""); +} + +const toHtml = text => blocks(text.replace(//g, "").split("\n")); + +/* ---------------------------------------------------------------- sections */ + +function sections(md) { + const out = []; + let cur = null; + for (const line of md.split("\n")) { + const m = line.match(/^##\s+(.*)$/); + if (m) { cur = { title: m[1].trim(), body: [] }; out.push(cur); continue; } + if (cur) cur.body.push(line); + } + return out.map(s => ({ ...s, body: s.body.join("\n") })); +} + +const ALL = sections(MD); +const titleOf = t => t.replace(/\*/g, "").replace(/\s+/g, " ").trim(); +const section = re => { + const s = ALL.find(x => re.test(x.title)); + if (!s) throw new Error(`${SOURCE}: no section matching ${re}`); + return s; +}; + +/* The tagline under the title, and STATUS's "not obvious" list, lead the first page: + they are what a stand-in GM needs before anything else. */ +const tagline = MD.split("\n## ")[0].split("\n").filter(l => /^>/.test(l)).join("\n"); +const notObvious = section(/^STATUS\b/).body.split(/^### /m) + .find(b => b.startsWith("The three things")); +if (!notObvious) throw new Error(`${SOURCE}: STATUS has lost "The three things that are not obvious"`); + +const ART = (id, alt) => + `
${par.split("\n").map(l => inline(l.trim())).join("
")}