# CREATURE FORGE — plan A tool for making NPCs and monsters that are **correct by construction, measured for lethality, and never baseline-human by default**. Plus art that costs nothing and cannot contradict the statblock. Status: **all five phases built** (23 September 2026). The table below is the state this plan was written against in September; it is kept because the reasoning depends on it, and every row of it has since moved: | then | now | |---|---| | 76 statblocks | 204, including 100 written in one pass | | 0 statblocks not `baseline` | non-baseline bodies are reachable and the forge defaults to them by role | | no combat simulator | `tools/simulate.mjs`, and six guards that measure against it | | no NPC portraits | 47 Midjourney plates and derived tokens in `art/portraits/` and `art/tokens/` | | ad-hoc validation | `creature-schema.mjs` plus `check-creatures`, `check-forge`, `check-armed` | Part 3 is `forge.mjs` (the derivation, at the root) and `tools/forge.mjs` (the CLI). Part 5 is `generateCreature` and the **Forge creature** button on the Actors sidebar. The two share one derivation, and `check-forge` requires them to agree when fed from their two different catalogues — see R-310 and the commit history for what that caught. **The in-Foundry panel has not been exercised in a running world.** The derivation is covered by 450 guarded combinations; `Actor.create`, the pack reads and the dialog are the part `check-behaviour` says outright it cannot reach. --- ## Where things actually stand | | | |---|---| | Statblocks in repo | 76, across `tools/content.mjs` (NPCS) and four scenario `*_CAST` arrays | | Species profiles the engine supports | 3 — `baseline`, `vesh`, `cadence` | | Statblocks using anything but `baseline` | **0** | | Combat simulator | **none committed** | | NPC portraits | **none** — `art/portraits/` holds the 16 PC portraits only | | Validation | `check-kits` and `check-scenarios`, each with its own ad-hoc resolve logic | Three findings drive the whole design. **1. The monster machinery is built and unused.** `RINGBRP.locationTables` defines `vesh` (bracing limbs, grasping limbs, a ridge instead of a head) and `cadence` (five bodies, no head, `frac: 0.28` each — a swarm that takes damage as a collective). Both have full hit-location tables and paper-doll figures. Every creature in the game is a `baseline` human anyway. The tool's job is not to make statblocks faster — it is to make *non-human* statblocks reachable. **2. The measured lethality tables cannot be reproduced.** THROUGH TRAIN Act Three publishes "MEASURED, 400 runs each, four duty-roster agents against the six" with a wipe rate of 24%. Nothing in the repo can produce that number. It is currently a claim, not a result, and the same is true of the starter's tables. **3. The art substrate already exists.** `RINGBRP.figures` holds per-species body geometry as SVG primitives — `head: { t: "ellipse", cx: 60, cy: 21, rx: 15, ry: 17 }` and so on — because the combat tab draws a paper doll from it. Portraits generated from that same data **cannot disagree with the creature's hit locations**, because they are the same numbers. --- ## Shape Five parts. Each ships alone and is useful alone. ``` tools/creature-schema.mjs the shape of a creature, and the only validator tools/simulate.mjs the lethality harness tools/forge.mjs the authoring CLI tools/make-portraits.mjs procedural art ringbrp.mjs (+ a template) the in-Foundry importer ``` The CLI is the source of truth: it writes spec objects into the repo, `npm run build` bakes them into packs, and the Foundry side only ever *reads* built packs. Nothing can be authored in the world and get lost outside git. --- ## Part 1 · The schema, and one definition of valid `tools/creature-schema.mjs` describes every field of a creature spec — the shape already in use, formalised, not changed. No churn to the 76 existing entries. What makes it worth doing is where the allowed values come from: they are **read live** from `SKILL_CATALOGUE`, `WEAPONS`, `ARMOURS`, `GEAR`, `TALENTS` and `RINGBRP.locationTables`. A skill or kit key that does not exist cannot be described as valid, because the catalogue is the enumeration. ```js export function validate(spec) // -> [] or [{ path, problem, nearest }] export function describe(field) // -> allowed values, for the CLI and for errors ``` `nearest` matters: a typo should come back as `unknown skill "stelth" — did you mean "stealth"?`, not as a stack trace three files away. **Then rewire the existing guards to call it.** `check-kits` and `check-scenarios` each carry their own resolve logic today; both become thin callers. One definition of valid, enforced in one place, which is the same argument `rules.mjs` already makes for the rules journal. *Ships as:* no behaviour change, all six guards still green, 76 creatures validate clean. If any of them do not, that is a bug found for free. --- ## Part 2 · The lethality harness `tools/simulate.mjs`. The centrepiece, and the reason to do this at all. **It must import the real rules, never reimplement them.** A simulator with its own copy of the combat loop measures a different game than the one being played, and it will drift silently. Everything comes from `rules.mjs` and `ringbrp.mjs`: - d100 roll-under, banded critical / special / success / failure / fumble, 96–99 always fail, 00 always fumbles, 1% floor - Reaction Lanes: `3 × max(⌈DEX/3⌉, ⌈readiness/10⌉)` less carried load, plus d6, **re-rolled every round including the first** - Graded defences: a defence stops a blow of its own quality or worse outright; against something better it reduces by as many steps as it was outclassed by; only a critical stops a critical - Hit locations from the creature's own species table; major wounds; damage modifier - Fire modes — slow shoots better then costs you, rapid bursts, beams held on target ``` node tools/simulate.mjs --creature keepers --party 4 --runs 400 --seed 11 node tools/simulate.mjs --creature keepers --party holloway,okonkwo,finch,rahimi node tools/simulate.mjs --sweep tier --target-wipe 0.10 ``` Seeded PRNG throughout, so a creature + party + seed always yields the same numbers. That is what makes a published table checkable rather than remembered. Output is the shape the scenarios already use — fraction hurt, fraction down, median rounds, wipe rate — so the existing tables can be **regenerated and corrected**, and `--sweep` answers the question the GM actually has: *what tier makes this a fight they survive four times in five?* Then `tools/check-lethality.mjs` joins the guards: every creature declares a band (`nuisance` / `dangerous` / `lethal`), and the build fails if it simulates outside it. A creature cannot quietly become a party-wiper between versions. *Ships as:* a seventh guard, plus regenerated measured tables in THROUGH_TRAIN and the starter. Expect the published numbers to move. That is the point. --- ## Part 3 · The forge CLI `tools/forge.mjs`. Deliberately **not** a form-filler — speed of typing is not the problem. It is a derivation tool, and its output is explainable. ``` node tools/forge.mjs new --species vesh --size large --role ambusher --tier 3 node tools/forge.mjs explain keepers node tools/forge.mjs write --into tools/scenario-throughtrain.mjs ``` `new` derives characteristics from the species profile and a size band, distributes skills from a role template, and picks kit that resolves — then validates its own output before printing. It emits a spec object as **source**, formatted to match the file it is going into, because the repo's whole posture is that content lives in code. `explain` prints which rule produced each number: *`STR 15` — large band (13–17), vesh median +2*. A statblock you cannot interrogate is one you cannot tune. Role templates and size bands live beside the schema so they are data, not code, and a `--tier` bump is a single multiplier a GM can reason about. --- ## Part 4 · Procedural portraits `tools/make-portraits.mjs`, modelled directly on `tools/make-icons.mjs` — which already generates all 292 icons as SVG with no external art. A portrait is composed, not drawn: - **Silhouette** from `RINGBRP.figures[species].parts`, scaled by SIZ. A `cadence` creature gets five bodies because its location table has five; a `vesh` gets a ridge instead of a head for the same reason. - **Tile and era treatment** reusing the icon set's existing four — antique notched, modern plain, future cut corner with a scan line, anomalous broken rim that does not close. A far-side thing looks far-side at 32px in an actor list. - **Detail layer** keyed on the creature's own data: limb count from the location table, plating from `naturalArmour`, a weapon silhouette from its primary weapon. - **Hue** from the icon set's category families, so a creature portrait sits beside the icons without clashing. Deterministic from the creature key, so the same creature always yields the same file and art does not churn in git. Output SVG, matching `icons/` — scalable, tiny, and diffable. ``` npm run portraits ``` **The honest trade.** These will read as diagrammatic — a species-accurate silhouette on a treated tile, not an illustration. They will not look like the 16 Midjourney PC portraits, and putting the two side by side will show it. Two ways out, neither needed on day one: regenerate the PC portraits procedurally too so the whole cast matches, or have the tool also emit a Midjourney prompt per creature so any individual one can be upgraded by hand later. The second is about an hour's work on top and worth taking. --- ## Part 5 · The Foundry importer A GM-only panel in the system module. Small on purpose. - Lists creatures from the built packs, filtered by species, tier and era - **Forge a variant**: apply a tier or size modifier to a pack creature and drop it into the current scene as an unlinked actor, for when the table goes somewhere the prep did not - Shows the simulated lethality band against the current party, read from the data the harness baked in It reads built packs only and writes nothing back to the repo, so it cannot diverge from the CLI. A variant forged at the table is a throwaway by design; if it earns a place, it gets authored properly in the morning. --- ## Order of work | Phase | Deliverable | Why here | | |---|---|---|---| | 1 | Schema + validator, guards rewired | Pure safety net. No new content, no risk, pays off immediately. | done | | 2 | Simulator + `check-lethality`, tables regenerated | Turns published claims into results. Biggest single win. | done | | 3 | Forge CLI | Needs 1 to validate against and 2 to tune against. | done | | 4 | Procedural portraits | Independent of 1–3; could move earlier if wanted. | done, and since replaced for 47 creatures by drawn plates | | 5 | Foundry panel | Needs built packs carrying lethality data, so it goes last. | built, **not yet run in a world** | Phases 1 and 2 are worth doing even if 3–5 never happen. --- ## Risks - **The simulator is the whole bet.** If it reimplements rules instead of importing them, it measures a fiction. Mitigation: import from `rules.mjs` only, and cross-check the harness against the 67 existing behavioural tests before trusting a single number. - **`vesh` and `cadence` have never been exercised.** No creature has ever used them, so the first non-baseline creature is also the first test of those code paths, in the engine *and* in the sheet rendering. Budget for engine bugs, not just tool bugs. - **Regenerated tables will contradict published ones.** THROUGH_TRAIN's 24% wipe rate may not survive measurement. Better found now than at a convention table, but the scenario prose will need revisiting. - **Portrait families will not match** until the PCs are regenerated or the prompt export is built. Covered above. - **Scope drift toward a full monster designer.** The chosen pillars are lethality, validation and range. Authoring speed was explicitly *not* one — resist the pretty form. --- *Private convention play materials — not for sale or distribution.*