The Custodians v0.6.1 — full system

A Foundry VTT game system: Basic Roleplaying d100 in a drab British agency that
deals with crossings, where reality keeps a record of what belongs where and can be
made to get it wrong.

Contents:
- rules.mjs as the single authority for every rule formula, with 40 rules and 132
  spot-checked values. Nothing may restate a formula it defines.
- 25 postings, each with a key characteristic, trained skills, talents and its own
  kit; a 175-item catalogue across four eras; 231 generated icons.
- Reaction Lanes initiative, re-rolled every round including the first.
- Coherence, and the Quiet Arts, which only ever arrive from a crossing gone wrong.
- Graded defences: only a critical stops a critical.
- Borrowed Authority — access as a record rather than a hacking roll.
- A case generator, and a rules journal generated from rules.mjs so the book cannot
  drift from the code.

Built compendia are committed so a remote Foundry needs only the files.
node_modules is not; `npm install` restores it for rebuilding content.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
slaguru666
2026-08-10 18:09:07 +01:00
co-authored by Claude Opus 5
commit d0144f744e
367 changed files with 20195 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
/**
* Every kit key in ROLES must name a real catalogue item.
*
* The generator used to match kit by fuzzy display name and drop what it could
* not find without saying so, which is how a Stores Officer came to be issued
* five things and receive one. Matching is now exact, so a typo is a silent
* omission instead — unless this runs.
*
* node tools/check-kits.mjs
*/
import { readFile } from "node:fs/promises";
import { SKILL_CATALOGUE, TALENTS, GEAR, WEAPONS, ARMOURS, DEVICES } from "./content.mjs";
const WEAPON_BY_KEY = new Map(WEAPONS.map(w => [w.key, w]));
const src = await readFile(new URL("../ringbrp.mjs", import.meta.url), "utf8");
const ROLES = eval("(" + src.match(/export const ROLES = (\{[\s\S]*?\n\});/)[1] + ")");
const skills = new Set(SKILL_CATALOGUE.map(s => (s[1] ? `${s[0]}:${s[1]}` : s[0])));
const talents = new Set(TALENTS.map(t => t.key));
const kit = new Set([...GEAR, ...WEAPONS, ...ARMOURS, ...(DEVICES ?? [])].map(x => x.key));
const stats = new Set(["str", "con", "siz", "int", "pow", "dex", "cha", "edu", "mas", "app"]);
const problems = [];
const kitSets = new Map();
const issued = new Map();
for (const [id, r] of Object.entries(ROLES)) {
const where = `${id} (${r.label})`;
if (!stats.has(r.stat)) problems.push(`${where}: stat "${r.stat}" is not a characteristic`);
for (const s of [...r.core, ...r.support]) if (!skills.has(s)) problems.push(`${where}: no skill "${s}"`);
for (const t of r.talents) if (!talents.has(t)) problems.push(`${where}: no talent "${t}"`);
for (const g of r.kit) {
if (!kit.has(g)) problems.push(`${where}: no catalogue item "${g}"`);
issued.set(g, (issued.get(g) ?? 0) + 1);
}
// A posting issued a weapon it has no skill for carries it at the 5% base. Ten
// of the twenty new roles did exactly that, and only a play-through found it —
// an Antiquities Officer drew a flintlock and rolled 5% with it.
const trained = new Set([...r.core, ...r.support]);
for (const k of r.kit) {
const w = WEAPON_BY_KEY.get(k);
if (!w?.fam || k === "punch") continue;
const need = w.spec ? `${w.fam}:${w.spec}` : w.fam;
if (!trained.has(need)) problems.push(`${where}: issued ${w.name}, trains no ${need}`);
}
const sig = JSON.stringify([...r.kit].sort());
if (kitSets.has(sig)) problems.push(`${where}: identical loadout to ${kitSets.get(sig)}`);
kitSets.set(sig, id);
}
// Every posting should be issued at least one thing no other posting gets. This
// is a design rule, not a correctness one, so it warns rather than fails.
const noSignature = Object.entries(ROLES)
.filter(([, r]) => !r.kit.some(g => issued.get(g) === 1))
.map(([id]) => id);
if (problems.length) {
console.error("check-kits: FAILED\n " + problems.join("\n "));
process.exit(1);
}
console.log(`check-kits: OK — ${Object.keys(ROLES).length} roles, `
+ `${kit.size} catalogue items, every kit key resolves, every posting can use what it carries, every loadout distinct`);
if (noSignature.length) console.warn(` note: no item unique to ${noSignature.join(", ")}`);