diff --git a/tools/all-specs.mjs b/tools/all-specs.mjs new file mode 100644 index 0000000..8998db1 --- /dev/null +++ b/tools/all-specs.mjs @@ -0,0 +1,57 @@ +/** + * Every actor spec in the repository, collected once. + * + * Three things now need the same list — check-creatures validates it, make-portraits + * draws it, and mj-queue writes prompts for it — and this repository's one standing law + * is that a definition may not exist twice. The collection rule is a definition: a + * scenario cast is any exported array whose entries carry characteristics, so a new + * scenario is covered the day it is written rather than the day somebody remembers to + * add it to a list. That rule belongs in one place. + * + * `kind` is what buildActor will make of the spec: "agent" becomes a character actor, + * "npc" becomes an npc actor. It decides which portrait family a creature is drawn in + * and which prompt template it gets, so it is carried here rather than re-derived. + */ +import { NPCS, PREGENS } from "./content.mjs"; +import { ROSTER } from "./roster.mjs"; + +/** Scenario modules whose casts join the list. */ +export const SCENARIOS = ["throughtrain", "starter", "lastadmission", "openday"]; + +/** + * @returns {Promise>} + * every spec, in a stable order, each tagged with where it came from. + */ +export async function collectSpecs() { + const sources = [ + { label: "content.mjs PREGENS", kind: "agent", specs: PREGENS }, + { label: "content.mjs NPCS", kind: "npc", specs: NPCS }, + { label: "roster.mjs ROSTER", kind: "agent", specs: ROSTER } + ]; + + for (const s of SCENARIOS) { + const mod = await import(`./scenario-${s}.mjs`); + for (const [name, value] of Object.entries(mod)) { + if (Array.isArray(value) && value.some(x => x && typeof x === "object" && x.ch)) { + sources.push({ label: `scenario-${s}.mjs ${name}`, kind: "npc", specs: value }); + } + } + } + + const out = []; + for (const { label, kind, specs } of sources) { + for (const spec of specs) out.push({ label, kind, spec }); + } + return out; +} + +/** The same thing grouped, for callers that report per source. */ +export async function collectSources() { + const flat = await collectSpecs(); + const byLabel = new Map(); + for (const row of flat) { + if (!byLabel.has(row.label)) byLabel.set(row.label, { label: row.label, kind: row.kind, specs: [] }); + byLabel.get(row.label).specs.push(row.spec); + } + return [...byLabel.values()]; +} diff --git a/tools/check-creatures.mjs b/tools/check-creatures.mjs index 70e26b1..c833601 100644 --- a/tools/check-creatures.mjs +++ b/tools/check-creatures.mjs @@ -14,30 +14,13 @@ * * node tools/check-creatures.mjs */ -import { NPCS, PREGENS } from "./content.mjs"; -import { ROSTER } from "./roster.mjs"; import { readFile } from "node:fs/promises"; import { validate, KNOWN_FIELDS } from "./creature-schema.mjs"; +import { collectSources } from "./all-specs.mjs"; -/* Scenario casts are collected by shape rather than by name: a cast is any exported - array whose entries carry characteristics. A new scenario is then covered the day it - is written instead of the day somebody remembers to add it here. */ -const SCENARIOS = ["throughtrain", "starter", "lastadmission", "openday"]; - -const sources = [ - { label: "content.mjs PREGENS", kind: "agent", specs: PREGENS }, - { label: "content.mjs NPCS", kind: "npc", specs: NPCS }, - { label: "roster.mjs ROSTER", kind: "agent", specs: ROSTER } -]; - -for (const s of SCENARIOS) { - const mod = await import(`./scenario-${s}.mjs`); - for (const [name, value] of Object.entries(mod)) { - if (Array.isArray(value) && value.some(x => x && typeof x === "object" && x.ch)) { - sources.push({ label: `scenario-${s}.mjs ${name}`, kind: "npc", specs: value }); - } - } -} +/* Scenario casts are collected by shape rather than by name — see all-specs.mjs, which + owns that rule now that make-portraits and mj-queue need the same list. */ +const sources = await collectSources(); const problems = []; const seenKeys = new Map();