STANDING ORDER: an Adventure compendium, built from the document
New pack ringbrp.standingorder, in the Scenarios folder. It holds the GM journal (13 pages, one per section), the three player handouts, nine plates to show, six faces and the Bellhouse section map as a scene. No cast actors, because nobody in the case has a stat block. The GM pages are read from docs/scenarios/STANDING_ORDER.md at build time rather than transcribed, so the compendium cannot drift from the document the desk passes ran against. STATUS and Open questions are left out as authoring notes; STATUS's "not obvious" list leads page 1. Doc fixes found on the way: the Act Two heading still said ~55 min against a 75-minute budget; STATUS said four passes and "no desk playtest run" after five; the runtime open question still called 45/55/50 unbacked. figures-baseline: generator enrolled at 0; STANDING_ORDER.md leaves the uncited list because it cites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e780181e07
commit
0fa21263b4
@@ -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.
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
MANIFEST-000002
|
||||
Binary file not shown.
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
"<p><em>Two hundred and six people went underground in 1974 to govern the country "
|
||||
+ "after the war. There was no war.</em></p>"
|
||||
+ "<p>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.</p>"
|
||||
+ "<p>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.</p>",
|
||||
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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, "<").replace(/>/g, ">");
|
||||
|
||||
function inline(s) {
|
||||
return esc(s)
|
||||
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
||||
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
|
||||
.replace(/(^|[^*\w])\*(?!\s)(.+?)\*(?!\w)/g, "$1<em>$2</em>");
|
||||
}
|
||||
|
||||
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(`<h${n}>${inline(m[2])}</h${n}>`);
|
||||
i++; continue;
|
||||
}
|
||||
if (/^\|/.test(line)) {
|
||||
const rows = [];
|
||||
while (i < lines.length && /^\|/.test(lines[i])) rows.push(lines[i++]);
|
||||
const body = rows.filter((r, k) => k !== 1 || !/^\|[\s:|-]+\|?\s*$/.test(r));
|
||||
const [head, ...rest] = body;
|
||||
const th = cells(head);
|
||||
const thead = th.some(Boolean) ? `<thead><tr>${th.map(c => `<th>${c}</th>`).join("")}</tr></thead>` : "";
|
||||
out.push(`<table>${thead}<tbody>${rest.map(r => `<tr>${cells(r).map(c => `<td>${c}</td>`).join("")}</tr>`).join("")}</tbody></table>`);
|
||||
continue;
|
||||
}
|
||||
if (/^>/.test(line)) {
|
||||
const inner = [];
|
||||
while (i < lines.length && /^>/.test(lines[i])) inner.push(lines[i++].replace(/^>\s?/, ""));
|
||||
const html = blocks(inner);
|
||||
const readAloud = /read aloud/i.test(inner[0] ?? "");
|
||||
out.push(`<blockquote${readAloud ? ' class="rb-readaloud"' : ""}>${html}</blockquote>`);
|
||||
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 => `<li>${inline(x.join(" "))}</li>`).join("")}</${tag}>`);
|
||||
continue;
|
||||
}
|
||||
const para = [];
|
||||
while (i < lines.length && lines[i].trim() && !/^(#{3,6}\s|\||>|[-*]\s|\d+\.\s|---+\s*$)/.test(lines[i])) {
|
||||
para.push(lines[i++].trim());
|
||||
}
|
||||
if (!para.length) { para.push(lines[i++].trim()); }
|
||||
out.push(`<p>${inline(para.join(" "))}</p>`);
|
||||
}
|
||||
return out.join("");
|
||||
}
|
||||
|
||||
const toHtml = text => blocks(text.replace(/<!--[\s\S]*?-->/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) =>
|
||||
`<figure class="rb-plate"><img src="systems/ringbrp/art/scenes/${id}.webp" alt="${alt}"/></figure>`;
|
||||
|
||||
export const SO_PAGES = [];
|
||||
{
|
||||
const essentials = section(/^GM ESSENTIALS/);
|
||||
SO_PAGES.push({
|
||||
name: "1 · Run this first",
|
||||
content: ART("so_01_cover", "Sheldon Moor, the vent head")
|
||||
+ toHtml(tagline)
|
||||
+ toHtml("### " + notObvious)
|
||||
+ "<h2>GM essentials</h2>" + toHtml(essentials.body)
|
||||
});
|
||||
let n = 2;
|
||||
for (const s of ALL) {
|
||||
if (s === essentials || SKIP.some(re => re.test(s.title))) continue;
|
||||
SO_PAGES.push({ name: `${n++} · ${titleOf(s.title)}`, content: toHtml(s.body) });
|
||||
}
|
||||
}
|
||||
|
||||
/* Player handouts: the quoted text of each SO-H, and nothing of the GM's "what it is
|
||||
for". The italic production line under each heading stays out too — it tells the GM
|
||||
when to hand it over, and tells the player nothing they should read. */
|
||||
export const SO_HANDOUTS = section(/^Handouts\b/).body.split(/^### /m).slice(1).map(chunk => {
|
||||
const [head, ...rest] = chunk.split("\n");
|
||||
const quote = rest.filter(l => /^>/.test(l)).map(l => l.replace(/^>\s?/, ""));
|
||||
if (!quote.length) throw new Error(`${SOURCE}: handout "${head}" has no quoted text`);
|
||||
const code = head.match(/^(SO-H\d+)/)?.[1];
|
||||
if (!code) throw new Error(`${SOURCE}: handout heading "${head}" has no SO-H code`);
|
||||
/* A handout is a typed page: its line breaks are the document's, so they are kept, and a
|
||||
blank quoted line is a paragraph break. */
|
||||
const body = quote.join("\n").split(/\n\s*\n/)
|
||||
.map(par => `<p>${par.split("\n").map(l => inline(l.trim())).join("<br>")}</p>`).join("");
|
||||
return { name: head.replace(/\s+—\s+/, " · ").trim(), content: body, code };
|
||||
});
|
||||
|
||||
export const SO_PLATES = [
|
||||
{ name: "1 · Sheldon Moor — the vent head", src: "art/scenes/so_01_cover.webp" },
|
||||
{ name: "2 · Wetherlaw farm", src: "art/scenes/so_02_wetherlaw.webp" },
|
||||
{ name: "3 · The ash edge", src: "art/scenes/so_03_ash_edge.webp" },
|
||||
{ name: "4 · The service adit", src: "art/scenes/so_04_adit.webp" },
|
||||
{ name: "5 · Bellhouse — the corridor", src: "art/scenes/so_05_corridor.webp" },
|
||||
{ name: "6 · The Control Room", src: "art/scenes/so_06_control_room.webp" },
|
||||
{ name: "7 · The Registry", src: "art/scenes/so_07_registry.webp" },
|
||||
{ name: "8 · The crèche", src: "art/scenes/so_08_creche.webp" },
|
||||
{ name: "9 · Bellhouse — section", src: "art/maps/bellhouse-section.webp" }
|
||||
];
|
||||
|
||||
/* The people, for showing. Joan Wetherlaw has no portrait yet — see STANDING_ORDER_ART. */
|
||||
export const SO_FACES = [
|
||||
{ name: "Controller Edwin Sowerby", src: "art/portraits/so_sowerby.webp" },
|
||||
{ name: "Registrar Alice Pennyfeather", src: "art/portraits/so_pennyfeather.webp" },
|
||||
{ name: "Dr Ivy Larkhall", src: "art/portraits/so_larkhall.webp" },
|
||||
{ name: "Deputy Controller Edith Marlow", src: "art/portraits/so_marlow.webp" },
|
||||
{ name: "Warden Michael Teague", src: "art/portraits/so_teague.webp" },
|
||||
{ name: "Sub-Officer Kit Marlow", src: "art/portraits/so_kit_marlow.webp" }
|
||||
];
|
||||
Reference in New Issue
Block a user