Files
RingBRP/tools/check-kits.mjs
T
slaguru666andClaude Opus 5 d9bce12c27 Replace the starter with BLACK PLATFORM, and make the dialogs ask questions
The starter is now an action case: a train running through a station whose rails were
lifted in 1976, two engineers aboard, eleven minutes until it returns. Descend, take the
platform, board, rescue, fight forward along a moving train, stop it.

Simpler than PAPER WINDOW on purpose — one clue scene, no social subsystem — which also
means it cannot reproduce R-248, because there are no groups or standings to leave
unwired.

Every agent can fight, deliberately: floors of firearm 42%, melee 40%, Brawl 40%,
Dodge 40% so nobody is a spectator on their first evening. Floors only RAISE, so a
posting already better stays better — Callum's shield lands at 53, not the floor's 45.

The builder now handles combat floors, extra weapons issued for a case, and a scenario
with NO Borrowed Authority at all; the last was assumed to exist and would have thrown.
Two new tests: every floor names a real skill, and every agent clears the fighting
floors the handout promises.

Sixty-two dialog strings reworded. They were naming mechanics at people: "Situational
modifier %" is now "Anything else helping or hurting? (%)", "Target cover" is "Is the
target behind anything?", "Difficult (÷2)" is "Difficult — half your skill". Labels ask
the question; hints say what it does to the number; notifications say what to do next
rather than what went wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 00:08:07 +01:00

111 lines
5.1 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 { SKILL_CATALOGUE, TALENTS, GEAR, WEAPONS, ARMOURS, DEVICES } from "./content.mjs";
import { SHIELD, SCOPE_TIERS } from "../rules.mjs";
// Imported, not regexed out of the engine source. See postings.mjs.
import { ROLES, TRADES, INDUCTION, CHARACTERISTIC_DICE } from "../postings.mjs";
const WEAPON_BY_KEY = new Map(WEAPONS.map(w => [w.key, w]));
const ARMOUR_BY_KEY = new Map(ARMOURS.map(a => [a.key, a]));
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));
// Derived, not retyped. The hand-written set still accepted "mas" and "app" —
// which are the DISPLAY NAMES of siz and cha, not keys — so a posting declaring
// stat: "mas" would have passed this guard and then produced an undefined
// characteristic at generation. Latent, but only because nobody had tried it.
const stats = new Set(Object.keys(CHARACTERISTIC_DICE));
const problems = [];
const kitSets = new Map();
const issued = new Map();
for (const [id, r] of Object.entries({ ...ROLES, ...TRADES })) {
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}`);
}
// A shield is armour you defend WITH, so it needs its skill for exactly the same
// reason a weapon does — a posting issued one and untrained in it parries at the
// 5% base and would be better off with both hands free.
const shieldSkill = `${SHIELD.parryFamily}:${SHIELD.parrySpec}`;
for (const k of r.kit) {
const a = ARMOUR_BY_KEY.get(k);
if (!a?.shield) continue;
if (!trained.has(shieldSkill)) {
problems.push(`${where}: issued ${a.name}, trains no ${shieldSkill}`);
}
}
const sig = JSON.stringify([...r.kit].sort());
if (kitSets.has(sig)) problems.push(`${where}: identical loadout to ${kitSets.get(sig)}`);
kitSets.set(sig, id);
}
// A Borrowed Authority whose scope is not a real tier reads as rank 0 — "nothing" —
// and every single action through it comes back "impossible" with no error anywhere.
// The starter scenario shipped with scope "operate", which is not a tier, and the only
// symptom was a credential that silently did not work.
{
const S = await import("./scenario-starter.mjs").catch(() => null);
const auth = S?.STARTER_AUTHORITY;
// A case may legitimately carry no credential; one that does must name a real tier.
if (auth && !SCOPE_TIERS.includes(auth.scope)) {
problems.push(`starter authority "${auth.name}": scope "${auth.scope}" is not a `
+ `scope tier (have: ${SCOPE_TIERS.join(", ")})`);
}
}
// INDUCTION is handed to every trade character, so its keys are checked once here
// rather than ten times above.
for (const k of INDUCTION.skills) {
if (!skills.has(k)) problems.push(`induction: no skill "${k}"`);
}
for (const g of INDUCTION.kit) {
if (!kit.has(g)) problems.push(`induction: no catalogue item "${g}"`);
const w = WEAPON_BY_KEY.get(g);
if (w?.fam) {
const need = w.spec ? `${w.fam}:${w.spec}` : w.fam;
if (!INDUCTION.skills.includes(need)) {
problems.push(`induction: issues ${w.name}, teaches no ${need}`);
}
}
}
// 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, `
+ `${Object.keys(TRADES).length} trades, `
+ `${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(", ")}`);