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>
45 lines
1.8 KiB
JavaScript
45 lines
1.8 KiB
JavaScript
/**
|
|
* A localisation key may be a LEAF or a BRANCH, never both.
|
|
*
|
|
* Foundry expands dotted keys into a nested object, so shipping both
|
|
* "RINGBRP.Authority.Merge" (a string) and "RINGBRP.Authority.Merge.0" makes it try
|
|
* to assign a property to a string. That throws inside the loader, and the loader
|
|
* abandons the WHOLE FILE — one bad pair silently un-localises the entire system and
|
|
* every sheet starts printing raw keys. It cost an hour to find once.
|
|
*
|
|
* node tools/check-lang.mjs
|
|
*/
|
|
import { readFile, readdir } from "node:fs/promises";
|
|
|
|
const dir = new URL("../lang/", import.meta.url);
|
|
let failures = 0;
|
|
|
|
for (const file of (await readdir(dir)).filter(f => f.endsWith(".json"))) {
|
|
const raw = await readFile(new URL(file, dir), "utf8");
|
|
let json;
|
|
try { json = JSON.parse(raw); }
|
|
catch (e) { console.error(`${file}: not valid JSON — ${e.message}`); failures++; continue; }
|
|
|
|
const keys = Object.keys(json);
|
|
const set = new Set(keys);
|
|
for (const k of keys) {
|
|
const parts = k.split(".");
|
|
for (let i = 1; i < parts.length; i++) {
|
|
const prefix = parts.slice(0, i).join(".");
|
|
if (set.has(prefix)) {
|
|
console.error(`${file}: "${prefix}" is a value AND a prefix of "${k}" — Foundry cannot load this file`);
|
|
failures++;
|
|
}
|
|
}
|
|
}
|
|
// duplicate keys survive JSON.parse by silently overwriting; catch them in the text
|
|
const seen = new Set(), dupes = new Set();
|
|
for (const m of raw.matchAll(/^\s*"([^"]+)"\s*:/gm)) {
|
|
if (seen.has(m[1])) dupes.add(m[1]); else seen.add(m[1]);
|
|
}
|
|
for (const d of dupes) { console.error(`${file}: duplicate key "${d}"`); failures++; }
|
|
if (!failures) console.log(`check-lang: OK — ${file}, ${keys.length} keys, no leaf/branch collisions`);
|
|
}
|
|
|
|
if (failures) { console.error(`\ncheck-lang: ${failures} failure(s)`); process.exit(1); }
|