It opened "Status: plan only. Nothing below is built" while four of the five phases had shipped, and its state table described a repo of 76 statblocks with no simulator and no portraits — 204, six measuring guards and 47 plates ago. A plan that misreports what exists sends the next reader to build it again. The table is kept rather than deleted because the design argues from it, with a second column saying where each row landed. Part 5 is marked built and NOT yet run in a world, which is the honest state. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
258 lines
12 KiB
Markdown
258 lines
12 KiB
Markdown
# 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 <key> --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.*
|