NPC kit: design for issuing the posting's stores to generated NPCs
generateNPC reads R.core, R.support and R.label. It does not read R.kit, which generateDialog prints as DRAWS FROM STORES and generateCharacter issues. The card promises a marksman rifle and a breaching suit; the actor gets a utility knife. Design: the posting says what it carries, the tier says how much of it and how good with it. Kit keys classify by which pack they resolve in rather than by a second hand-written list. Attacks derive from the issued weapon's own skillFamilyId/skillSpecialisationId, which retires the weapon-to-skill pairing table. Armour becomes an equipped item and naturalArmour drops to 0 on the four human tiers, which moves the numbers and so gets measured. check-generator.mjs is written first and observed failing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
750cbd257d
commit
8b088fb3f0
@@ -0,0 +1,198 @@
|
||||
# NPC KIT — design
|
||||
|
||||
Generated NPCs arrive with no gear, no armour and one of six weapons, while the dialog
|
||||
that generates them prints the full loadout they were supposed to draw. This closes that
|
||||
gap, and takes the weapon vocabulary from six keys to whatever the posting actually carries.
|
||||
|
||||
Status: design approved, not yet built.
|
||||
|
||||
---
|
||||
|
||||
## The defect
|
||||
|
||||
Every one of the 36 postings in `postings.mjs` carries a `kit` array — weapon, armour and
|
||||
gear keys:
|
||||
|
||||
```js
|
||||
heavy: { label: "Containment", …
|
||||
kit: ["rifle_med","breach_suit","prybar","riot_shield","restraints",
|
||||
"climbing_kit","bolt_croppers","cordon_kit","ear_defenders"] }
|
||||
```
|
||||
|
||||
Three places read it. `generateDialog` renders it on the role card as **DRAWS FROM
|
||||
STORES** (`ringbrp.mjs:4512`). `generateCharacter` issues it (`ringbrp.mjs:3994`).
|
||||
`expand-spec.mjs` issues it for packed agents.
|
||||
|
||||
`generateNPC` reads `R.core`, `R.support` and `R.label`, and nothing else.
|
||||
|
||||
So the card in the dialog promises a marksman rifle, a breaching suit, an entry bar, a
|
||||
riot shield, restraints, breaching charges, bolt croppers, cordon tape and ear defenders —
|
||||
and the actor it creates is handed a utility knife. The card is not wrong about the
|
||||
posting; it is describing a kit the NPC path drops on the floor.
|
||||
|
||||
Two consequences follow from the same root:
|
||||
|
||||
- **Six weapons, total.** `THREATS.attacks` hardcodes `dagger`, `pistol_med`,
|
||||
`rifle_med`, `baton`, `grasp`, `unmaker` across all six tiers, against a catalogue of
|
||||
70. A Containment officer cannot be issued the rifle the posting is built around,
|
||||
because the tier chose the weapon and the tier has never heard of the posting.
|
||||
- **Armour is a number, not a thing.** `naturalArmour: { value: TH.armour }` is set on
|
||||
every tier including the human ones. There is no item on the sheet, so a breaching suit
|
||||
cannot be seen, stripped, burned, or reasoned about by location.
|
||||
|
||||
The content is not the bottleneck. `tools/content.mjs` already holds 66 skills, 70
|
||||
weapons, 35 armours, 132 gear and 22 talents. The NPC path is drinking through a straw.
|
||||
|
||||
---
|
||||
|
||||
## Principle
|
||||
|
||||
**The posting says what it carries. The tier says how much of it, and how good with it.**
|
||||
|
||||
Nothing below adds a hand-written list of items, because a hand-written list of valid
|
||||
values drifts and the drift is invisible — the argument `creature-schema.mjs` already
|
||||
makes, and the reason the schema derives its vocabularies from the catalogues rather than
|
||||
retyping them. Every item an NPC receives is a key already written in the posting.
|
||||
|
||||
---
|
||||
|
||||
## Part 1 · Classify kit keys by pack membership
|
||||
|
||||
`generateNPC` already opens `ringbrp.weapons` and `ringbrp.skills`. It gains `ringbrp.armour`
|
||||
and `ringbrp.gear`, and builds one key → document map per pack from `flags.ringbrp.key`.
|
||||
|
||||
A kit key resolves to exactly one map, and that is its class. There is no second list
|
||||
saying which keys are weapons; the pack it lives in is the answer. A key in no pack is
|
||||
skipped with a console warning naming it — the same posture `warnKey` already takes,
|
||||
because a silent substitution during a case is worse than a noisy one.
|
||||
|
||||
Classify by pack, never by reading the key. Containment's `climbing_kit` is named
|
||||
**"Breaching charges"**, and three other keys in that one posting differ from their
|
||||
display names (`rifle_med` is "Marksman rifle", `breach_suit` is "Breaching suit",
|
||||
`prybar` is "Entry bar"). The key is an identifier and has drifted from the name it was
|
||||
coined for; anything that infers meaning from the spelling of a key will be wrong here
|
||||
first. The card is faithful because it looks the name up rather than prettifying the key.
|
||||
|
||||
---
|
||||
|
||||
## Part 2 · The tier draws from the posting's stores
|
||||
|
||||
`THREATS` gains one field per tier:
|
||||
|
||||
```js
|
||||
draw: { weapons: 1, armour: 1, gear: 3 }
|
||||
```
|
||||
|
||||
| Tier | weapons | armour | gear | note |
|
||||
|---|---|---|---|---|
|
||||
| Bystander | 0 | 0 | 2 | a publican has pockets, not a sidearm |
|
||||
| Trained | 1 | 1 | 3 | |
|
||||
| Armed | 1 | 1 | 3 | |
|
||||
| Dangerous | 2 | 1 | 4 | a primary and a sidearm |
|
||||
| Anomalous | — | — | — | natural attacks; draws no issued kit |
|
||||
| Apex | — | — | — | as above |
|
||||
|
||||
Weapons are taken in the order the posting lists them, because kits are authored primary
|
||||
first. Gear is drawn at random from the posting's gear so that four guards off one
|
||||
statblock are not four identical guards.
|
||||
|
||||
**Creature tiers draw nothing.** A revenant does not sign for a riot shield. Anomalous and
|
||||
Apex keep the `attacks` they have and keep `naturalArmour`. This is a judgement call and
|
||||
the one most likely to be wrong: the dialog lets a posting and a threat tier be chosen
|
||||
independently, so "Apex · Containment" is selectable and will produce a creature with
|
||||
Containment's *skills* and none of its kit. Say the word if you want kit on those too.
|
||||
|
||||
---
|
||||
|
||||
## Part 3 · Attacks derive from what was issued
|
||||
|
||||
This is the part that removes a whole class of drift rather than patching it.
|
||||
|
||||
Every weapon item already names the skill that fires it:
|
||||
|
||||
```js
|
||||
system: { …, skillFamilyId: w.fam, skillSpecialisationId: w.spec, … }
|
||||
```
|
||||
|
||||
So the weapon↔skill pairing table is unnecessary. For each weapon issued, read those two
|
||||
fields and grant that skill at the tier's band. A generated NPC cannot hold something it
|
||||
cannot use, because the thing it is holding is what names the skill.
|
||||
|
||||
`THREATS.attacks` survives for the creature tiers only, where there is no posting kit to
|
||||
draw from. The comment above it — written when eight Apexes in ten had no usable attack —
|
||||
stays true and now applies to a smaller surface.
|
||||
|
||||
The existing unarmed fallback (R-213: issue `punch` + `brawl` at a reduced rating to
|
||||
anyone whose only attack is a firearm) is unchanged and still runs last.
|
||||
|
||||
---
|
||||
|
||||
## Part 4 · Armour becomes an item
|
||||
|
||||
Issued armour is created equipped, like `buildActor` does it.
|
||||
|
||||
`naturalArmour` drops to 0 for the four human tiers, because the number was standing in
|
||||
for the item and keeping both would armour every NPC twice. It stays for Anomalous and
|
||||
Apex, which is what it was for.
|
||||
|
||||
**This changes how hard generated NPCs are to hurt**, in both directions — a Trained NPC
|
||||
goes from a flat 1 to whatever a stab vest stops by location, which is more in the chest
|
||||
and nothing in the head. `check-lethality` measures packed creatures, not generated ones,
|
||||
so it will not catch this. The honest test is `tools/simulate.mjs` against a generated
|
||||
cohort, and the numbers go in the review entry.
|
||||
|
||||
Overload is not a new problem: `stowOverload` already exists for the PC path and is
|
||||
reused, so a Containment officer issued four gear items is not permanently slowed by
|
||||
owning her own breaching charges.
|
||||
|
||||
---
|
||||
|
||||
## Part 5 · The guard that would have caught this
|
||||
|
||||
A new `tools/check-generator.mjs`, registered in `npm run check` as guard 24.
|
||||
|
||||
For every posting × tier it asserts:
|
||||
|
||||
1. Every weapon key the role card prints is a key that resolves in a pack — the card
|
||||
cannot advertise an item that does not exist.
|
||||
2. Every weapon issued to the actor arrives with the skill named in its own
|
||||
`skillFamilyId`/`skillSpecialisationId`.
|
||||
3. A tier whose `draw.weapons` is non-zero receives at least one weapon from the posting's
|
||||
kit — the assertion that fails today.
|
||||
4. The issued set is a subset of the posting's kit plus the tier's natural attacks. The
|
||||
generator may issue less than the card shows; it may never issue something the card
|
||||
never mentioned.
|
||||
|
||||
**Written first, and observed failing against current `ringbrp.mjs`.** A guard that passes
|
||||
before the fix is measuring something other than the defect — and this repo has spent
|
||||
enough of this month on checks that were green for the wrong reason.
|
||||
|
||||
---
|
||||
|
||||
## Order of work
|
||||
|
||||
| Phase | Deliverable | Why here |
|
||||
|---|---|---|
|
||||
| 1 | `check-generator.mjs`, failing | Proves the defect is where the design says it is |
|
||||
| 2 | Pack classification + `draw` tables | No behaviour change until Phase 3 reads them |
|
||||
| 3 | `generateNPC` issues kit; attacks derive from it | The fix. Guard goes green |
|
||||
| 4 | Armour as item, `naturalArmour` to 0 on human tiers | Separate because it moves numbers |
|
||||
| 5 | Simulated before/after, review entry R-310 | The balance change, measured not asserted |
|
||||
|
||||
Phases 1–3 are worth shipping alone; 4 is the part that needs numbers behind it.
|
||||
|
||||
---
|
||||
|
||||
## What this is not
|
||||
|
||||
Not the monster builder. `docs/CREATURE_FORGE_PLAN.md` covers that, and its Phases 1, 2
|
||||
and 4 are already built — schema and validator, simulator and lethality guard, procedural
|
||||
portraits. What is missing there is **Phase 3 (`tools/forge.mjs`, absent)** and **Phase 5
|
||||
(the in-Foundry importer, absent)**, which together are the authoring experience being
|
||||
asked for. That plan explicitly warned against "scope drift toward a full monster
|
||||
designer"; wanting to build monsters in the app is a decision that overrides it, but it
|
||||
should be taken knowingly and as its own piece of work, after this one lands.
|
||||
|
||||
---
|
||||
|
||||
*Private convention play materials — not for sale or distribution.*
|
||||
Reference in New Issue
Block a user