Files
RingBRP/tools/bellhouse-plans.mjs
T
slaguru666andClaude Opus 5 33f2b62070 STANDING ORDER: seven blueprint scenes of the underground
Drawn by tools/bellhouse-plans.mjs from declared rooms: lobby, control
room, registry, sick bay, creche, vigil hall and plant, coal roads and
the Sump. 2800x2000 at 5 ft to the square, added to the Adventure and
keyed in the Locations table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 11:43:02 +01:00

429 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* STANDING ORDER — the Bellhouse blueprints: seven floor plans of the Seat, one per
* Foundry scene.
*
* Drawn from declared rooms rather than generated, for culland-hall.svg's reason: a room,
* a door and a label are load-bearing, and a generator cannot be trusted with any of them.
* Every plan is 2800x2000 at 100px to the square (5 ft), so the Foundry grid lands on the
* drawn one. Labels are what a 1962 drawing office would have written, plus the pencil
* amendments of fifty years below; nothing on a plan tells the players a GM secret.
*
* node tools/bellhouse-plans.mjs write art/maps/bh-*.svg
* node tools/bellhouse-plans.mjs --render and render each to .webp (rsvg-convert, cwebp)
*/
import { writeFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
import path from "node:path";
const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
const OUT = path.join(ROOT, "art", "maps");
const S = 100, W = 28, H = 20;
const esc = s => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const p = n => +(n * S).toFixed(1);
/* ---------------------------------------------------------------- primitives */
function sheet() {
const g = [];
const api = {
g,
room(x, y, w, h, label, { sub, n, cls = "rm", lx = 0.3, ly = 0.75 } = {}) {
g.push(`<rect x="${p(x)}" y="${p(y)}" width="${p(w)}" height="${p(h)}" class="${cls}"/>`);
if (label) g.push(`<text x="${p(x + lx)}" y="${p(y + ly)}" class="lbl">${esc(label)}</text>`);
if (sub) g.push(`<text x="${p(x + lx)}" y="${p(y + ly + 0.38)}" class="sub">${esc(sub)}</text>`);
if (n != null) {
g.push(`<circle cx="${p(x + w - 0.45)}" cy="${p(y + 0.45)}" r="30" class="numc"/>`
+ `<text x="${p(x + w - 0.45)}" y="${p(y + 0.45) + 13}" class="num">${n}</text>`);
}
},
/* A door on a wall: `v` for a vertical wall at x, else horizontal at y. A gap in the
wall and a quarter-circle swing, the way a drawing office draws one. */
door(x, y, v, len = 1, flip = false) {
const L = p(len), X = p(x), Y = p(y), d = flip ? -1 : 1;
if (v) {
g.push(`<line x1="${X}" y1="${Y}" x2="${X}" y2="${Y + L}" class="gap"/>`,
`<path d="M${X} ${Y}L${X + d * L} ${Y}A${L} ${L} 0 0 ${d > 0 ? 1 : 0} ${X} ${Y + L}" class="swing"/>`);
} else {
g.push(`<line x1="${X}" y1="${Y}" x2="${X + L}" y2="${Y}" class="gap"/>`,
`<path d="M${X} ${Y}L${X} ${Y + d * L}A${L} ${L} 0 0 ${d > 0 ? 0 : 1} ${X + L} ${Y}" class="swing"/>`);
}
},
/* An open archway: just the gap. */
arch(x, y, v, len = 2, cls = "gap") {
const X = p(x), Y = p(y), L = p(len);
g.push(v ? `<line x1="${X}" y1="${Y}" x2="${X}" y2="${Y + L}" class="${cls}"/>`
: `<line x1="${X}" y1="${Y}" x2="${X + L}" y2="${Y}" class="${cls}"/>`);
},
box(x, y, w, h, label, size = 20) {
g.push(`<rect x="${p(x)}" y="${p(y)}" width="${p(w)}" height="${p(h)}" class="fur"/>`);
if (label) g.push(`<text x="${p(x + w / 2)}" y="${p(y + h / 2) + size * 0.35}" class="fl" font-size="${size}">${esc(label)}</text>`);
},
chair(x, y) { g.push(`<rect x="${p(x)}" y="${p(y)}" width="34" height="34" rx="6" class="fur"/>`); },
bed(x, y, vert = true, w = 0.9, l = 2) {
const [bw, bh] = vert ? [w, l] : [l, w];
g.push(`<rect x="${p(x)}" y="${p(y)}" width="${p(bw)}" height="${p(bh)}" class="fur"/>`);
g.push(vert ? `<line x1="${p(x)}" y1="${p(y + 0.45)}" x2="${p(x + bw)}" y2="${p(y + 0.45)}" class="fine"/>`
: `<line x1="${p(x + 0.45)}" y1="${p(y)}" x2="${p(x + 0.45)}" y2="${p(y + bh)}" class="fine"/>`);
},
shelf(x, y, w, h) {
g.push(`<rect x="${p(x)}" y="${p(y)}" width="${p(w)}" height="${p(h)}" class="fur"/>`);
const lines = [];
if (w >= h) for (let i = x + 0.25; i < x + w; i += 0.25) lines.push(`M${p(i)} ${p(y)}V${p(y + h)}`);
else for (let j = y + 0.25; j < y + h; j += 0.25) lines.push(`M${p(x)} ${p(j)}H${p(x + w)}`);
g.push(`<path d="${lines.join("")}" class="fine"/>`);
},
reels(x, y, label = "TAPE") {
g.push(`<rect x="${p(x)}" y="${p(y)}" width="200" height="120" class="fur"/>`,
`<circle cx="${p(x + 0.5)}" cy="${p(y + 0.5)}" r="32" class="fine"/>`,
`<circle cx="${p(x + 1.5)}" cy="${p(y + 0.5)}" r="32" class="fine"/>`,
`<text x="${p(x + 1)}" y="${p(y + 1.12)}" class="fl" font-size="15">${esc(label)}</text>`);
},
safe(x, y) {
g.push(`<rect x="${p(x)}" y="${p(y)}" width="80" height="80" class="fur"/>`,
`<path d="M${p(x)} ${p(y)}l80 80M${p(x) + 80} ${p(y)}l-80 80" class="fine"/>`,
`<text x="${p(x + 0.4)}" y="${p(y + 1.15)}" class="fl" font-size="16">SAFE</text>`);
},
speaker(x, y) {
g.push(`<circle cx="${p(x)}" cy="${p(y)}" r="16" class="fur"/>`,
`<path d="M${p(x) + 24} ${p(y) - 16}a24 24 0 0 1 0 32M${p(x) + 34} ${p(y) - 26}a38 38 0 0 1 0 52" class="fine"/>`);
},
round(x, y, r, label) {
g.push(`<circle cx="${p(x)}" cy="${p(y)}" r="${p(r)}" class="fur"/>`);
if (label) g.push(`<text x="${p(x)}" y="${p(y) + 7}" class="fl" font-size="18">${esc(label)}</text>`);
},
stairs(x, y, w, h, label = "STAIR") {
g.push(`<rect x="${p(x)}" y="${p(y)}" width="${p(w)}" height="${p(h)}" class="rm2"/>`);
const d = [];
for (let j = y + 0.3; j < y + h; j += 0.3) d.push(`M${p(x)} ${p(j)}H${p(x + w)}`);
g.push(`<path d="${d.join("")}" class="fine"/>`,
`<text x="${p(x + w / 2)}" y="${p(y + h / 2) + 8}" class="fl bg" font-size="20">${esc(label)}</text>`);
},
blast(x, y, h) {
g.push(`<rect x="${p(x) - 22}" y="${p(y)}" width="44" height="${p(h)}" class="blast"/>`);
const d = [];
for (let j = y; j < y + h; j += 0.2) d.push(`M${p(x) - 22} ${p(j) + 20}l44 -20`);
g.push(`<path d="${d.join("")}" class="fine"/>`);
},
/* The drawing office's own lettering, outside a room. */
text(x, y, t, cls = "sub", extra = "") {
g.push(`<text x="${p(x)}" y="${p(y)}" class="${cls}" ${extra}>${esc(t)}</text>`);
},
/* Pencil: fifty years of amendments in a different hand. */
pencil(x, y, t, rot = -2) {
g.push(`<text x="${p(x)}" y="${p(y)}" class="pen" transform="rotate(${rot} ${p(x)} ${p(y)})">${esc(t)}</text>`);
},
arrow(x1, y1, x2, y2) {
g.push(`<line x1="${p(x1)}" y1="${p(y1)}" x2="${p(x2)}" y2="${p(y2)}" class="arw" marker-end="url(#ah)"/>`);
},
raw(s) { g.push(s); }
};
return api;
}
const STYLE = `
.rm { fill:#154a8a; stroke:#e8f1fb; stroke-width:7; }
.rm2 { fill:#11417d; stroke:#e8f1fb; stroke-width:5; }
.rock { fill:#0e3668; stroke:#9fbde0; stroke-width:4; stroke-dasharray:18 10; }
.gap { stroke:#154a8a; stroke-width:12; }
.rgap { stroke:#0e3668; stroke-width:12; }
.swing { fill:none; stroke:#e8f1fb; stroke-width:3; }
.fur { fill:#1b579c; stroke:#cfe0f3; stroke-width:3; }
.fine { fill:none; stroke:#cfe0f3; stroke-width:2; }
.blast { fill:#0a2f5c; stroke:#e8f1fb; stroke-width:4; }
.arw { stroke:#e8f1fb; stroke-width:4; }
.grid { stroke:#2a5d9c; stroke-width:1; }
.numc { fill:#e8f1fb; }
.num { font-family:'Courier New',Courier,monospace; font-weight:700; font-size:36px;
fill:#123f78; text-anchor:middle; }
.lbl { font-family:'Courier New',Courier,monospace; font-weight:700; font-size:30px;
fill:#f4f8fd; letter-spacing:2px; }
.sub { font-family:'Courier New',Courier,monospace; font-size:21px; fill:#b9d0ea; }
.fl { font-family:'Courier New',Courier,monospace; font-size:18px; fill:#e8f1fb;
text-anchor:middle; }
.ttl { font-family:'Courier New',Courier,monospace; font-weight:700; font-size:34px;
fill:#f4f8fd; letter-spacing:3px; }
.tb { font-family:'Courier New',Courier,monospace; font-size:20px; fill:#dbe8f6; }
.pen { font-family:'Bradley Hand','Noteworthy','Marker Felt',cursive; font-size:27px;
fill:#ffe98a; }
`;
function render(no, title, level, draw, { legend = [1, 15.6] } = {}) {
const s = sheet();
draw(s);
const grid = [];
for (let i = 1; i < W; i++) grid.push(`M${i * S} 0V${H * S}`);
for (let j = 1; j < H; j++) grid.push(`M0 ${j * S}H${W * S}`);
// Title block, bottom right, as a drawing office would box it.
const tb = [19, 16.5, 8.7, 3.3];
const [tx, ty, tw, th] = tb.map(p);
const block = `
<rect x="${tx}" y="${ty}" width="${tw}" height="${th}" fill="#0f3a70" stroke="#e8f1fb" stroke-width="5"/>
<line x1="${tx}" y1="${ty + 110}" x2="${tx + tw}" y2="${ty + 110}" stroke="#e8f1fb" stroke-width="3"/>
<line x1="${tx}" y1="${ty + 250}" x2="${tx + tw}" y2="${ty + 250}" stroke="#e8f1fb" stroke-width="3"/>
<text x="${tx + 24}" y="${ty + 44}" class="tb">REGIONAL SEAT OF GOVERNMENT 12 · BELLHOUSE</text>
<text x="${tx + 24}" y="${ty + 88}" class="tb">SHEET ${no} OF 7 · ${esc(level)}</text>
<text x="${tx + 24}" y="${ty + 165}" class="ttl">${esc(title)}</text>
<text x="${tx + 24}" y="${ty + 216}" class="tb">SCALE: ONE SQUARE = 5 FT</text>
<text x="${tx + 24}" y="${ty + 300}" class="tb">DRG RSG12/${String(no).padStart(2, "0")} · ISSUED 1962 · COPY NOT TO LEAVE THE SEAT</text>`;
// Legend, bottom left.
const [lx, ly] = legend.map(p);
const key = `
<rect x="${lx}" y="${ly}" width="640" height="${p(3.9) - 10}" fill="#0f3a70" stroke="#e8f1fb" stroke-width="3"/>
<text x="${lx + 24}" y="${ly + 44}" class="tb" font-weight="700">KEY</text>
<path d="M${lx + 30} ${ly + 80}L${lx + 30} ${ly + 150}A70 70 0 0 0 ${lx + 100} ${ly + 80}" class="swing"/>
<line x1="${lx + 30}" y1="${ly + 80}" x2="${lx + 100}" y2="${ly + 80}" stroke="#e8f1fb" stroke-width="7"/>
<text x="${lx + 130}" y="${ly + 120}" class="tb">DOOR</text>
<circle cx="${lx + 360}" cy="${ly + 110}" r="26" class="numc"/>
<text x="${lx + 360}" y="${ly + 122}" class="num" font-size="30">7</text>
<text x="${lx + 400}" y="${ly + 120}" class="tb">LOCATION No.</text>
<text x="${lx + 30}" y="${ly + 200}" class="pen" font-size="25">pencil</text>
<text x="${lx + 130}" y="${ly + 200}" class="tb">= AMENDED AT THE SEAT, 1974–2026</text>
<rect x="${lx + 30}" y="${ly + 232}" width="70" height="44" class="rock"/>
<text x="${lx + 130}" y="${ly + 262}" class="tb">= UNLINED MINE WORKINGS</text>
<text x="${lx + 30}" y="${ly + 330}" class="tb">1 SQUARE = 5 FT · N ↑</text>`;
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W * S}" height="${H * S}" viewBox="0 0 ${W * S} ${H * S}">
<!-- STANDING ORDER — ${esc(title)}. Generated by tools/bellhouse-plans.mjs; edit that, not this. -->
<defs>
<style>${STYLE}</style>
<marker id="ah" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M0 0L10 5L0 10z" fill="#e8f1fb"/>
</marker>
</defs>
<rect width="${W * S}" height="${H * S}" fill="#123f78"/>
<path d="${grid.join("")}" class="grid"/>
${s.g.join("\n ")}
${key}
${block}
</svg>
`;
}
/* ---------------------------------------------------------------- the sheets */
export const PLANS = [
{ key: "bh-lobby", no: 1, title: "LOBBY & ENQUIRIES", level: "LEVEL 1 · ENTRY", draw: s => {
s.text(0.3, 0.6, "FROM ADIT (SEALED 1975)");
s.room(1, 6, 4, 6, "BLAST LOCK", { sub: "outer lock" });
s.blast(1, 8, 2);
s.raw(`<circle cx="${p(3)}" cy="${p(10)}" r="${p(1.1)}" fill="none" stroke="#ffe98a" stroke-width="4" stroke-dasharray="14 10"/>`);
s.pencil(1.6, 11.6, "air goes wrong here", -3);
s.room(5, 6, 4, 6, "DECONTAM.", { sub: "showers · lockers" });
for (const y of [8, 9.3, 10.6]) s.round(6, y, 0.3);
s.shelf(7.9, 7.4, 0.8, 4.2);
s.room(9, 3, 10, 12, "LOBBY", { n: 6, sub: "public waiting" });
s.box(14.5, 5.2, 3.8, 0.8, "ENQUIRIES COUNTER");
s.box(17.5, 6, 0.8, 2.4);
for (const x of [10, 11, 12]) for (const y of [9, 11, 13]) { s.chair(x, y); }
s.box(9.8, 8.4, 2.9, 0.4); s.box(9.8, 10.4, 2.9, 0.4); s.box(9.8, 12.4, 2.9, 0.4);
s.box(15, 14.4, 3.6, 0.35, "NOTICEBOARD", 14);
s.pencil(14.6, 13.9, "housing list — see Mr Ogden", -1);
s.room(19, 3, 5, 5, "ENQUIRIES", { sub: "Mr Ogden" });
s.box(20, 4.6, 2, 1, "DESK"); s.chair(21, 5.8); s.shelf(23.1, 3.6, 0.7, 3.6);
s.round(20.3, 7.2, 0.25, "☎");
s.room(19, 8, 5, 4, "INTERVIEW", { sub: "the door does not lock" });
s.box(20.4, 9.8, 2.2, 1.1, "TABLE");
s.chair(20.6, 9.3); s.chair(21.8, 9.3); s.chair(20.6, 11); s.chair(21.8, 11);
s.room(19, 12, 5, 3, "WARDENS' POST");
s.shelf(19.4, 13.4, 2.5, 0.5); s.text(19.4, 14.6, "truncheon rack", "sub");
s.room(12, 0, 3, 3, null, { cls: "rm2" }); s.text(12.2, 1.3, "MAIN", "lbl"); s.text(12.2, 1.9, "CORRIDOR", "sub");
s.arrow(13.5, 2.6, 13.5, 0.3);
s.door(5, 8.5, true, 1); s.door(9, 8.5, true, 1);
s.door(19, 4.8, true, 1, true); s.door(19, 9.2, true, 1, true); s.door(19, 13, true, 1, true);
s.arch(12.5, 3, false, 2);
s.arrow(0.5, 0.9, 0.5, 5.5);
}},
{ key: "bh-control", no: 2, title: "CONTROL ROOM", level: "LEVEL 2 · GOVERNMENT", draw: s => {
s.room(0.5, 1, 27, 2, "MAIN CORRIDOR", { cls: "rm2", ly: 1.25 });
s.room(1, 4, 12, 9, "CONTROL ROOM", { n: 7, sub: "Controller's floor" });
s.box(1.8, 5.5, 10.4, 0.9, "CONSOLE · DIALS · TANNOY DESK");
for (let x = 2.5; x < 12; x += 1.4) s.chair(x, 6.6);
s.box(3.5, 8, 6, 2.6, "SITUATION MAP · REGION 12", 20);
for (const [x, y] of [[3.8, 7.4], [5.6, 7.4], [7.4, 7.4], [3.8, 10.8], [5.6, 10.8], [7.4, 10.8]]) s.chair(x, y);
s.round(11.4, 11.4, 0.55, "SCOPE");
s.pencil(9.8, 12.6, "SEALED 1974 — by order", -2);
s.room(14, 4, 6, 5, "CONTROLLER", { sub: "E. Sowerby" });
s.box(15.2, 5.8, 2.4, 1.1, "DESK"); s.chair(16.1, 7.1);
s.shelf(19.1, 4.6, 0.7, 3.8); s.text(17.4, 8.7, "letters from Centre", "sub");
s.room(14, 9, 6, 6, "STUDIO", { sub: "broadcast · 1962" });
s.reels(14.6, 10.6, "TAPE DECK");
s.safe(18.6, 10.2);
s.box(15, 12.6, 2.6, 0.9, "MIXER"); s.chair(15.9, 13.7);
s.round(18.8, 13.6, 0.3, "MIC");
s.pencil(14.3, 14.7, "new tape rec'd from Centre 2019", -1);
s.room(21, 4, 6, 5, "PIPE ROOM", { n: 11, sub: "Centre's in-tray" });
s.round(24, 6.6, 0.55, "PIPE");
s.box(21.5, 6.1, 1.8, 0.8, "IN-TRAY");
s.pencil(21.4, 8.7, "every 90 days", -2);
s.room(21, 9, 6, 6, "COMMS", { sub: "teleprinters · switchboard" });
s.box(21.6, 10.8, 1.3, 1, "TP"); s.box(23.2, 10.8, 1.3, 1, "TP");
s.box(21.6, 12.6, 4.8, 0.9, "SWITCHBOARD"); s.chair(22.3, 13.8); s.chair(24.3, 13.8);
s.pencil(24.6, 10.6, "no line out", 4);
s.door(3, 4, false, 1, true); s.door(16, 4, false, 1, true); s.door(23, 4, false, 1, true);
s.door(14, 11, true, 1, true); s.door(14.8, 9, false, 1, true); s.door(23.5, 9, false, 1, true);
}},
{ key: "bh-registry", no: 3, title: "REGISTRY", level: "LEVEL 2 · GOVERNMENT", draw: s => {
s.room(0.5, 1, 27, 2, "MAIN CORRIDOR", { cls: "rm2", ly: 1.25 });
s.room(1, 4, 9, 7, "PUBLIC COUNTER", { n: 8 });
s.box(1, 7.4, 6.5, 0.7, "COUNTER");
for (const x of [2, 3.5, 5, 6.5]) s.chair(x, 5.6);
s.box(5.4, 4.3, 2, 0.45, "TAKE A NUMBER", 13);
s.pencil(1.4, 10.6, "complaints: allow 11 weeks", -2);
s.room(10, 4, 12, 11, "RECORDS");
for (let x = 11; x < 21.5; x += 1.5) s.shelf(x, 5.6, 0.6, 7.6);
s.text(10.4, 14.5, "BIRTHS · DEATHS · HOUSING · RATIONS · 1974–", "sub");
s.room(22, 4, 5, 5, "REGISTRAR", { sub: "A. Pennyfeather" });
s.box(23, 5.8, 2.2, 1, "DESK"); s.chair(23.8, 7.1);
s.room(22, 9, 5, 6, "STRONGROOM", { sub: "standing orders" });
s.safe(25.6, 9.9); s.shelf(22.5, 11.2, 3.8, 0.7); s.shelf(22.5, 13.3, 3.8, 0.7);
s.room(1, 11, 9, 4, "TYPING POOL");
for (const x of [1.8, 4.3, 6.8]) { s.box(x, 12.3, 1.8, 0.8); s.chair(x + 0.7, 13.4); }
s.door(3.6, 4, false, 1, true); s.door(24, 4, false, 1, true);
s.door(10, 5.2, true, 1, true); s.door(22, 6, true, 1, true); s.door(24.8, 9, false, 0.9, true);
s.arch(8, 7.4, true, 0.7);
s.door(4, 11, false, 1);
}},
{ key: "bh-sickbay", no: 4, title: "SICK BAY", level: "LEVEL 3 · WELFARE", draw: s => {
s.room(0.5, 1, 27, 2, "WELFARE CORRIDOR", { cls: "rm2", ly: 1.25 });
s.room(1, 4, 6, 5, "RECEPTION", { n: 9 });
s.box(1.6, 5.7, 3, 0.7, "DESK");
for (const x of [1.6, 2.6, 3.6, 4.6]) s.chair(x, 7.1);
s.room(7, 4, 12, 6, "GENERAL WARD", { sub: "eighteen beds" });
for (let x = 7.5; x < 17; x += 1.1) { s.bed(x, 5.2); s.bed(x, 7.9, true, 0.9, 1.9); }
s.room(19, 4, 4, 3, "SIDE ROOM", { sub: "Mrs Marlow" });
s.bed(21.8, 4.3, true, 0.9, 1.8); s.chair(21, 5.4); s.shelf(19.3, 6.2, 1.6, 0.5);
s.room(23, 4, 4, 3, "DISPENSARY");
s.shelf(23.3, 5.5, 3.4, 0.6);
s.room(19, 7, 8, 3, "MEDICAL OFFICER", { sub: "Dr Larkhall" });
s.box(23, 8.2, 2, 1, "DESK"); s.shelf(26.1, 7.3, 0.6, 2.4); s.chair(23.7, 9.3);
s.room(7, 10, 7, 5, "DELIVERY", { sub: "maternity" });
s.bed(10, 11.8, false, 1, 2.2); s.round(12.9, 12.4, 0.35, "COT");
s.pencil(7.4, 14.6, "Cotter — due Thurs", -2);
s.room(14, 10, 6, 5, "THEATRE");
s.box(15.8, 11.6, 2.4, 1, "TABLE"); s.round(18.9, 11, 0.4); s.shelf(14.3, 14, 3, 0.6);
s.room(1, 9, 6, 6, "ISOLATION");
s.room(1, 11, 3, 4, null); s.room(4, 11, 3, 4, null);
s.bed(1.8, 12.4); s.bed(4.8, 12.4);
s.pencil(1.3, 10.6, "unused since '78", -1);
s.door(3.5, 4, false, 1, true); s.door(24.5, 4, false, 1, true);
s.door(7, 6, true, 1, true); s.door(19, 5, true, 1, true); s.door(19, 8.2, true, 1, true);
s.door(10, 10, false, 1); s.door(16.2, 10, false, 1);
s.door(3.2, 9, false, 1, true); s.door(2.5, 11, false, 0.8); s.door(5.2, 11, false, 0.8);
s.door(21.2, 7, false, 0.8, true);
}},
{ key: "bh-creche", no: 5, title: "CRECHE & SCHOOL", level: "LEVEL 3 · WELFARE", draw: s => {
s.room(0.5, 1, 27, 2, "WELFARE CORRIDOR", { cls: "rm2", ly: 1.25 });
s.room(1, 4, 5, 4, "CLOAKS");
for (let y = 5.3; y < 7.8; y += 0.5) s.raw(`<circle cx="${p(1.3)}" cy="${p(y)}" r="7" fill="#e8f1fb"/>`);
s.room(1, 8, 5, 5, "STAFF", { sub: "K. Marlow" });
s.bed(1.5, 10.2); s.box(3.4, 9.6, 1.8, 0.8, "DESK");
s.room(6, 4, 10, 7, "SCHOOLROOM", { n: 10 });
s.box(7, 5.2, 5, 0.25, "", 12); s.text(7, 5.9, "BLACKBOARD", "sub");
s.box(12.8, 5.6, 2, 0.9, "TEACHER");
for (let x = 7; x < 15; x += 2) for (let y = 6.9; y < 10; y += 1.2) { s.box(x, y, 1.3, 0.6); }
s.pencil(12.4, 10.7, "atlas: 1971 edition", -2);
s.room(16, 4, 11, 7, "PLAYROOM");
s.raw(`<rect x="${p(16.2)}" y="${p(4.2)}" width="${p(10.6)}" height="16" fill="#e8f1fb" opacity=".5"/>`);
s.text(16.3, 5.4, "pinned drawings along this wall", "sub");
s.box(18, 6.6, 4, 2.6, "RUG", 20);
for (const [x, y] of [[23.5, 7], [25.4, 6.4], [24.6, 8.8], [17.2, 9.9]]) s.round(x, y, 0.35);
s.box(25.3, 9.6, 1.4, 1.1, "TOYS", 16);
s.room(6, 11, 14, 5, "DORMITORY", { sub: "under-twelves" });
for (let x = 6.6; x < 19.5; x += 0.95) { s.bed(x, 12.3, true, 0.7, 1.3); s.bed(x, 14.3, true, 0.7, 1.3); }
s.speaker(13, 11.35);
s.pencil(13.8, 11.95, "speaker fitted 2019 — 03:00 nightly", -1);
s.room(20, 11, 7, 5, "WASHROOM");
for (let x = 21.8; x < 26.8; x += 1.2) s.round(x, 12.9, 0.3);
s.box(21, 14, 5, 0.8, "BATHS");
s.door(3, 4, false, 1, true); s.door(6, 5, true, 1); s.door(16, 6.4, true, 1);
s.door(10.4, 11, false, 1); s.door(2.8, 8, false, 1); s.door(6, 11.6, true, 1, true);
s.door(20, 13, true, 1);
}, legend: [0.3, 16.1] },
{ key: "bh-vigil", no: 6, title: "VIGIL HALL & PLANT", level: "LEVEL 3 · ASSEMBLY", draw: s => {
s.room(1, 1, 14, 13, "VIGIL HALL", { n: 13, ly: 12.5 });
s.box(3, 1.5, 10, 1.8, "PLATFORM", 22);
s.box(7.6, 3.4, 0.8, 0.5, "", 12); s.text(8.6, 3.9, "lectern", "sub");
s.box(3.5, 1.1, 5, 0.3, "", 10);
s.text(9, 1.35, "ROLL OF HONOUR", "sub");
for (let y = 4.8; y < 11.5; y += 0.8) for (let x = 2; x < 12.8; x += 0.55) {
if (x > 7.4 && x < 8.3) continue;
s.raw(`<rect x="${p(x)}" y="${p(y)}" width="36" height="34" rx="5" class="fur"/>`);
}
s.speaker(1.5, 4.5); s.speaker(1.5, 10);
s.pencil(9.6, 12.4, "999 seats — add one", -2);
s.room(15, 1, 2, 18, null, { cls: "rm2" });
s.text(15.3, 17.4, "CORR.", "sub");
s.room(17, 1, 10, 9, "PLANT FLOOR", { n: 12 });
s.box(17.8, 2.4, 3.4, 2.6, "COAL BOILER");
s.box(22, 2.4, 2.2, 2, "GEN 1"); s.box(24.6, 2.4, 2.2, 2, "GEN 2");
s.box(17.8, 6.2, 8.6, 0.6, "DUCTING");
s.box(17.8, 7.6, 2.4, 1.6, "PUMPS"); s.box(21, 7.6, 2.4, 1.6, "FILTERS");
s.pencil(23.6, 8.8, "coal from No.2 road", -2);
s.room(17, 10, 10, 5.5, "TRANSMITTER", { sub: "feed to mast" });
s.shelf(18, 11.8, 5, 0.9); s.text(18, 13.2, "valve racks", "sub");
s.box(24, 11.6, 2.2, 1.4, "PANEL");
s.raw(`<line x1="${p(26.5)}" y1="${p(12.3)}" x2="${p(26.5)}" y2="${p(10.3)}" class="arw" marker-end="url(#ah)"/>`);
s.text(24.3, 15.1, "MAST CABLE ↑ MOOR", "sub");
s.door(15, 5.5, true, 1); s.door(15, 11, true, 1);
s.door(17, 4, true, 1, true); s.door(17, 12, true, 1, true);
s.arrow(16, 18.8, 16, 19.9); s.arrow(16, 1.2, 16, 0.1);
}, legend: [1, 15.6] },
{ key: "bh-sump", no: 7, title: "TUNNELS & SUMP", level: "LEVEL 4 · WORKINGS", draw: s => {
s.room(0.5, 1.5, 27, 3, "No.2 COAL ROAD", { cls: "rock", n: 14, sub: "growing beds under lamps" });
for (let x = 6; x < 26; x += 2.4) s.shelf(x, 2.2, 2, 0.6);
for (let x = 6; x < 26; x += 2.4) s.shelf(x, 3.7, 2, 0.6);
s.pencil(20, 1.3, "workings beyond: not surveyed", -1);
s.room(9, 4.5, 2, 3, null, { cls: "rock" }); s.room(17, 4.5, 2, 3, null, { cls: "rock" });
s.room(0.5, 7.5, 18.5, 3, "No.3 COAL ROAD", { cls: "rock", sub: "trout tanks" });
for (let x = 6; x < 16.5; x += 2.8) s.box(x, 8.1, 2.3, 1.8, "TANK", 16);
s.room(19, 6.5, 8.5, 5, "HEN RUN", { cls: "rock" });
for (let x = 20; x < 27; x += 1.3) s.box(x, 9.9, 0.9, 0.9);
s.pencil(20.3, 8.6, "eggs → Welfare, Tues/Fri", -2);
s.stairs(13, 10.5, 3, 4, "SUMP STAIR");
s.room(1, 11.5, 12, 2, "THE SUMP", { n: 15, ly: 1.3 });
for (const x of [1, 4, 7, 10]) s.room(x, 13.5, 3, 4.5, null);
for (const [i, x] of [1, 4, 7, 10].entries()) {
s.text(x + 0.3, 14.1, `CELL ${i + 1}`, "sub");
s.bed(x + 0.5, 15.3);
s.door(x + 1.9, 13.5, false, 0.9);
}
s.box(8.8, 11.9, 2, 0.8, "WARDEN");
s.pencil(1.3, 18.6, "locks: 1962 pattern — oil them", -1);
s.arch(13.5, 10.5, false, 2, "rgap"); s.arch(13, 12, true, 1);
s.arch(9, 4.5, false, 2, "rgap"); s.arch(9, 7.5, false, 2, "rgap");
s.arch(17, 4.5, false, 2, "rgap"); s.arch(17, 7.5, false, 2, "rgap");
s.door(19, 8.5, true, 1);
s.arrow(1.3, 3, 0.1, 3);
s.text(0.6, 1.2, "← to Welfare stair", "sub");
}, legend: [17.5, 12.1] }
];
/* The legend on sheet 7 sits above the title block; the title block never moves. */
if (import.meta.url === `file://${process.argv[1]}`) {
const doRender = process.argv.includes("--render");
for (const pl of PLANS) {
const svg = render(pl.no, pl.title, pl.level, pl.draw, pl.legend ? { legend: pl.legend } : {});
const svgPath = path.join(OUT, `${pl.key}.svg`);
writeFileSync(svgPath, svg);
if (doRender) {
const png = path.join(OUT, `${pl.key}.png`);
execFileSync("rsvg-convert", ["-w", "2800", "-h", "2000", svgPath, "-o", png]);
execFileSync("cwebp", ["-quiet", "-q", "88", png, "-o", path.join(OUT, `${pl.key}.webp`)]);
execFileSync("rm", [png]);
}
console.log(` ${pl.key}${doRender ? " (+webp)" : ""}`);
}
}