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>
65 lines
3.0 KiB
JavaScript
65 lines
3.0 KiB
JavaScript
/**
|
|
* 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(", ")}`);
|