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>
429 lines
23 KiB
JavaScript
429 lines
23 KiB
JavaScript
/**
|
||
* 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, "&").replace(/</g, "<").replace(/>/g, ">");
|
||
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)" : ""}`);
|
||
}
|
||
}
|