Files
RingBRP/docs/REVIEW_LOG.md
T
slaguru666andClaude Opus 5 7f8355f0d0 Add the player primer and cheat sheet, and brand every journal
Ten pages a player reads before session one, in their own compendium at OBSERVER
ownership: nine of setting, one cheat sheet.

The draft called the organisation "the Bureau"; everything else in the game calls it
the department or the agency, so that is what it is called. Thin areas filled rather
than padded: the seven character principles as a list, the real Coherence band table
instead of a paragraph describing one, and "what continuity is not" pulled out of the
middle of a page because it is the most important sentence for a new player.

The cheat sheet's numbers are computed from rules.mjs and lang/en.json at build time.
A hand-typed one would have been the sixth time this project shipped a number that
disagreed with the code, and the one the players were holding.

R-250: the design tokens were never on :root. Journals rendered as default parchment,
and four things were wrong in sequence — v14's hook is renderJournalEntrySheet; its
`element` is the node, so `[0]` returns the first CHILD and put the class on a header
button; there is no .journal-page-content in v14; and then the real one: with the
selector matching, font-size, line-height and padding all applied while every colour
did not, because the palette is declared on .ringbrp-sheet/.ringbrp-chat/.ringbrp-window
and never on :root. A journal inherited no tokens, so every var() silently became
nothing. A rule that HALF applies is not a specificity problem — I spent three attempts
treating it as one, including a version bump on a cache theory that was wrong.

Branded: primer, rulebook, starter GM text and handouts. Scoped by flag, so other
modules' journals are untouched. Version 0.9.0.

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

5133 lines
294 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RingBRP — design review log
## Round 1 — Codex review of RINGBRP_RULES_SPEC_v0.1.md
**Date:** 7 August 2026 · **Reviewer:** Codex (GPT-5.4) via `codex exec` · **Author:** Claude (Opus 5)
### Verdict headline
> "The draft has a sound BRP chassis, but it currently fails its own Ringworld test. The generic
> five-tier technology rule and generic progress-clock research system replace distinctive
> Ringworld procedures with generic sci-fi mechanics."
> "Without those changes, this remains competent generic BRP science fiction wearing Ringworld nouns."
That is exactly the failure mode the spec asked Codex to hunt for, and it found it.
### Factual errors found — VERIFIED against the source corpus
Nine of Codex's checkable claims were re-checked by Claude against `~/RingBRP/extracted/`.
**All nine were correct.** Claude's spec was wrong in each case.
| # | Codex's claim | Source check | Verdict |
|---|---|---|---|
| 1 | 1984 general HP = **CON + MAS**, not an average | Explorer Book: "hit points equal to the total of his CON + his MAS" | Codex right; `R-09` halved it |
| 2 | 1984 has **seven** hit locations (L/R arm, L/R leg, abdomen, chest, head) + general HP = 8 live values | Explorer Book: lists all seven by name | Codex right; spec said "six pools" |
| 3 | POW doubles = **open-ended reroll-and-add** for Earth humans under 100, attributed to Puppeteer breeding | Explorer Book, POW entry | Codex right; spec said "may indicate psi potential" |
| 4 | Default age die is **1D8**; 1D10 reserved for NPCs/special cases | Explorer Book: "roll 1D8 … ordinarily the gamemaster should not allow such ancient explorers" | Codex right; spec said 1D10 |
| 5 | `Ringworld` skill covers **macrostructure and physical relationships only** — explicitly "equivalent to Planetology" — not artefact identification | Explorer Book, RINGWORLD (S) | Codex right; spec's "Cthulhu Mythos slot" was a genericisation |
| 6 | BRP difficulty ladder is **Automatic / Easy (×2) / Average / Difficult (÷2) / Impossible** | BRP:UGE p.113 and glossary | Codex right; spec's +50/+20/−20/−50/×⅕ was invented |
| 7 | BRP: cannot mix Parry and Dodge in one round unless completely defensive; cumulative −30% per extra defence | BRP:UGE Dodge entry | Codex right; `R-19` omitted the restriction |
| 8 | Stunners resolve on **energy output vs target MAS**, not CON vs potency | Technology Book, STUNNERS | Codex right |
| 9 | Variable sword **cannot** cut scrith, GP hull or stasis fields, and cuts dense alloys slowly | Technology Book, variable knife entry | Codex right; spec said "ignores all non-powered armour" |
Additional errors accepted without independent check (internally consistent and checkable later):
- `R-01` fumble formula does not reproduce BRP. Correct form:
`fumble_start = 101 - ceil((101 - clamp(effective,1,100)) / 20)`
(verified by arithmetic against BRP's own worked example: skill 60 → 98–00.)
- `R-22` rounding: BRP requires `ceil` for HP, Major Wound and Experience Bonus; spec used
`round`/`floor`/`floor`.
- Major Wound is caused by **one blow** meeting the threshold, not by crossing a current-HP boundary.
- `R-22` cross-references `R-09`, which is about age, not HP. Broken reference.
- `R-14` unfamiliar-specialisation transfer is catastrophically broad — would grant
Science (Nuclear Physics) at 60% off Science (Biology) at 80%.
- `R-15` xeno-cap as written needs hidden per-skill learning-source provenance — i.e. it
reintroduces the parallel economy it claimed to delete.
- `R-16` contains 36 rows, not 35; omits Navigate and Research.
- `R-23` encumbrance base should be **STR** kg (spec said MAS), with local-gravity multiplication.
- `R-24`/`R-26` directly contradict each other on whether tier-gap-3 needs research.
- `R-27` advancement omits the Experience Bonus from the improvement roll and mis-states the
over-100% rule.
- `P-3`/`P-5` overstated the diagnosis: xenological activation spent ordinary occupation points,
not a separate currency, and was expressible on a flat sheet.
### Decision Record verdicts
| DR | Codex | Note |
|---|---|---|
| DR-01 delete impulse combat | **AGREE** | Add opposed Athletics/Pilot chase exchange for the second-by-second beat |
| DR-02 keep MAS/APP names | **AGREE-WITH-CHANGE** | Canonical engine IDs `siz`/`cha`; MAS/APP as display labels in the setting layer only |
| DR-03 consolidate skills | **AGREE-WITH-CHANGE** | Keep families but require real specialties; add Navigate and Research; publish a finite launch catalogue |
| DR-04 move ranged to Combat | **DISAGREE** | Keep firearms/energy under Perception; bows/thrown under Combat. The 1984 placement was deliberate — marksmanship as concentration, not agility |
| DR-05 xeno table as training cap | **AGREE-WITH-CHANGE** | Cap `Xenology (Species)` only; Xenology then augments other skills |
| DR-06 TECH_TIER_GAP | **DISAGREE** | Replace with familiarity: familiar 0 / analogous −20 / no analogue −50 / Builder locked until a named Discovery, then −20. Materials and weapons get explicit traits |
| DR-07 retain Research | **AGREE-WITH-CHANGE** | Rebuild as 1–5 **typed sequential steps** (field, library, laboratory, analysis). Novel alien subjects require ≥1 field step. Completion grants a Discovery, not automatic skill dice |
| DR-08 ×5 attribute rolls | **AGREE** | Lethality is controlled by HP/damage/armour, not by Idea/Luck/Stamina |
| DR-09 keep Ringworld Lore | **AGREE-WITH-CHANGE** | Keep it, but restore the narrow macrostructure remit |
| DR-10 separate mechanics/setting layers | **AGREE** | Worth the naming overhead |
### Open Question positions
| Q | Codex position |
|---|---|
| Q-01 multiplier | ×5, BRP standard |
| Q-02 hit locations | **Pools off; roll a location only when a blow causes a Major Wound**, then apply a concise consequence. Preserves severing, autodocs and variable-sword horror without seven HP tracks |
| Q-03 defence | BRP default: unlimited same-type at cumulative −30%, no mixing parry/dodge, keep missile-dodge restrictions |
| Q-04 encumbrance | STR kg; each 10 kg over: −1 MOV, −5% Physical/Manipulation/Combat; multiply by local gravity |
| Q-05 opposed rolls | Full BRP, including the loser downgrading the winner's degree |
| Q-06 playable species | Human, Kzin, Puppeteer at launch. Native guide as an NPC template; native PCs later with a warning that they change the premise |
| Q-07 automatic effects | Yes — a formal `resolution: automatic` category with explicit immunity/counter tags. Not GM fiat |
| Q-08 scale | **"Yes; this is the largest missing subsystem."** Light expedition procedure: journeys split into 3–6 legs, each with time cost, resource cost and one Navigation/Pilot test. Track exactly three expedition resources: **Supply, Power, Vehicle Integrity** |
| Q-09 Foundry | Skills as embedded Items with `familyId`/`specialisationId`/`categoryId`/`base`/`value`/`trainingCap`/`transferGroup` fields — **do not parse names**. Research as a **shared Project Item on a party/expedition Actor**, not per-PC |
| Q-10 missing | First-contact procedure; Navigate and Research skills; expedition travel; vehicle/starship scale and damage; material traits; technological traditions; the young-Earth POW rule; simweb training; gravity adaptation and zero-g; and actual species/equipment/pursuit data |
### Over-deletion (put it back)
- The four 1984 research **modes** (field, library, laboratory, armchair/analysis) — especially the
rule that major research generates a field expedition.
- `Ringworld` skill's narrow macrostructural remit.
- Young-Earth open-ended POW; simweb training; local-gravity encumbrance; gravity adaptation.
- Location **consequences** — losing them guts variable swords, autodocs and transplant resistance.
- The shared handling group linking flashlight laser and variable sword.
- Publication and assimilation of xenological discoveries back in Human Space as a downtime reward.
### Under-deletion (still too complex)
- Full BRP characteristic-based category modifiers — six calculated sheet values. Use BRP's
Simple Skill Category Bonuses, or drop category bonuses entirely.
- `R-14` — recreates root-skill leakage in a worse form.
- 36 skills plus unbounded free-text specialties can exceed the original sheet burden.
- `R-25` research — up to 50 progress points, repeated rolls, two difficulty mechanisms.
- Five universal tech tiers — "an unnecessary taxonomy with little setting truth".
- Pursuit packages plus separate allocation pools. Give each pursuit a fixed package plus at
most three player-selected increases; do not reopen point-buy per career.
- Use BRP's "remove statements" option as the default combat mode: declare and resolve on DEX rank.
### Foundry blockers
1. `R-24` — contradictory permissions, no defined unlock scope.
2. `R-25` — no step durations, difficulty modifiers, progress floor or ownership.
3. Species, pursuits, equipment, armour, weapons, defects and vehicles are referenced but have
no schemas and no data.
### Agreed direction for v0.2
1. Delete `TECH_TIER_GAP` as written; replace with technological-tradition familiarity plus
explicit material/weapon traits.
2. Rebuild Research as typed, expedition-generating steps producing Discoveries.
3. Restore `Ringworld` skill's real remit.
4. Add the expedition/travel procedure (Supply / Power / Vehicle Integrity).
5. Add a first-contact procedure.
6. Fix all arithmetic and BRP-conformance errors listed above.
### Outstanding disagreements from round 1, needing the human owner's call
- **DR-04** (ranged weapons: Perception vs Combat category) — Claude proposed Combat for BRP
conformity; Codex wants Perception preserved for setting fidelity. Genuine design split.
- **DR-06** (tech gap model) — Claude's tier taxonomy vs Codex's familiarity model. Codex's is
better; noted for adoption unless the owner prefers the simpler tier table.
---
## Round 2 — Codex review of RINGBRP_RULES_SPEC_v0.2.md
**Date:** 7 August 2026 · **Reviewer:** Codex (GPT-5.4) · **Author:** Claude (Opus 5)
### Headline — the round-1 verdict is reversed
> **"No, it is no longer generic BRP sci-fi wearing Ringworld nouns."**
Evidence Codex cited, all mechanical rather than cosmetic: open-ended Earth-human POW expressing
Puppeteer breeding (`R-05a`); age/longevity/multi-pursuit generation producing Known Space
characters (`R-09`, `R-29`); Megastructure Lore with its exact 1%-on-arrival macrostructural
role (`R-17`); Ring distance consuming Power the local civilisation cannot replace (`R-31`);
field expeditions producing Discoveries that unlock specific Builder functions (`R-25`/`R-26`);
publication creating a return-to-Human-Space loop (`R-26a`); scrith, GP hull and stasis fields
changing what weapons can physically do (`R-24c`).
> "The distinctive spine exists. The problem is now execution, not identity."
### CRITICAL — v0.2 defeats the owner's own ruling
`R-12a` made **Simple Category Modifiers the default**, and BRP's simple modifiers use the
**primary characteristic only**. So the default Ranged Combat bonus is `DEX/2` — and **POW and
CON contribute nothing**. The owner ruled that ranged weapons use DEX *and* the Perception
characteristics. The full-modifier table honours that; the default does not.
Codex's fix, adopted: **Ranged simple bonus = `ceil((DEX + POW + CON) / 6)`**, and by symmetry
Melee = `ceil((STR + DEX) / 4)`. Must be corrected in v0.3.
### Regression check — round-1 errors
**FIXED:** fumble formula · difficulty ladder · opposed downgrade · POW rule · HP source and
rounding · broken cross-reference · `R-14` transfer breadth · `R-15` xeno-cap provenance ·
`R-17` Ringworld remit · `R-19` parry/dodge exclusivity · `R-23` STR base · `R-24`/`R-26`
contradiction · research architecture and reward · stunner and variable sword · `P-3`/`P-5`
retraction · monofilament transfer group · publication reward.
**NOT FIXED / NEW PROBLEMS:**
| Issue | Detail |
|---|---|
| `R-12a` | Defeats the owner's ranged-weapon ruling (above). **Highest priority.** |
| `R-27` simweb | Invokes "special failure", but `R-01` has no special-failure band. Map to fumble or the rule is unusable. |
| `R-27` experience | Allows experience on "any skill" while also saying Research is the only route past 100% for Mental skills. Add per-skill `experienceEligible`. |
| `R-25` library hours | Should be **100 hours**, not 200. *(Verified against source: "Normally, the time needed is 100 hours per step.")* |
| `R-25` multi-researcher | "ANY success completes / ANY fumble does not" is self-contradictory. Ruling: the success completes the step, the fumble still imposes its complication. |
| `R-25` new specialisation | "(highest related − 0)" is unclear and far too generous. Start new specialisations at printed base. |
| `R-10` | Promises location consequences but supplies **no table**. Rolling a location then rolling BRP's Major Wound table can contradict itself (location "arm", result "facial scar, lose CHA"). Write the seven-location consequence table. |
| `DR-01` chase | Still no opposed Athletics/Pilot pursuit procedure. Expedition legs are not combat chases. |
| `R-08a` / `R-24d` | Zero-g and vacuum are asserted as "familiarities" with no effects, acquisition rules or schema. |
| `R-30` | Automatic-effect powers have labels but no rule for counter matching, precedence, or where immunity tags live. |
| §8 | Character familiarity state, species, pursuit, weapon, armour, defect and vehicle schemas still absent. Deferring the *data* to v0.3 is fine; deferring the *schemas* is a v0.2 blocker. |
### The four new subsystems
**`R-24` Tradition and familiarity — right mechanic, wrong granularity.**
Numbers endorsed: keep 0 / −20 / −50 / locked (at skill 60 that reads 60% / 40% / 10%, a legible
spread). But analogy and unlocking belong to a **function**, not a whole device — a Builder
console can be analogous for *operate*, alien for *diagnose*, impossible for *repair*. Also, one
tradition per device cannot model hybrid Known Space tech. Restructure to
`functions.{operate,diagnose,repair}: {analogueIn, requiredDiscoveryIds}` with `traditionIds` as
an array.
**`R-25` Research — generates play, calendar-heavy but not table-heavy, internally inconsistent.**
Codex's timing analysis: a four-step solo project at skill 50 averages ~1,600 hours (160
ten-hour days); three researchers at 50% give an 87.5% per-step success chance, cutting it to
~92 days. Defensible if it is downtime. Needs: per-step `difficulty` (default Average, currently
referenced but never stored), a cap on participating rollers, stated objectives for field steps
(`resolveInPlay: true` is not an objective), and five as the maximum *initial* length with
fumble-inserted setback steps permitted beyond it.
**`R-31` Expedition — light enough, but costs and skill roles are broken.**
- "−1 supply, −1 power; vehicle legs −1 power instead of −1 supply" is ambiguous. Ruling: foot leg
−1 Supply; vehicle leg −1 Supply **and** −1 Power. People do not stop eating inside vehicles.
- Allowing Pilot *instead of* Navigate lets a good driver find a rumoured destination without
navigating. Navigate governs every overland leg; Pilot mitigates hazards or a failed Navigate.
- Failure currently lets the party take extra time whenever time is cheap, dodging resource
pressure. Make failure cost baseline **plus one resource** and +50% elapsed time.
- Fumble: pay baseline, no progress, −1 Integrity, hazard.
- Critical should find a Site only on an **unexplored** leg, or players farm Sites on repeat runs.
- Recovery has no numbers: one day, one roll — success +2, special +3, critical +4.
- One Integrity track cannot describe several vehicles. Either name it abstract or track per asset.
**`R-32` First contact — generates a scene, but READ is weak and Standing moves far too fast.**
The maths: at skill 60, only a special-or-better READ reveals the fact that makes OFFER Easy —
**12% of attempts**. The other 48 points of success merely report disposition. That contradicts
the claim that READ is the highest-value action. Fix: success reveals one taboo *or* desire;
special reveals both; critical reveals both plus +20% to a later stage. Remove Standing changes
from READ — silently understanding someone should not make them like you. Cap permanent Standing
change at **±1 per encounter**; +3 should need at least three contacts. And OFFER needs
communication gating: if SIGNAL failed with no shared language, verbal Bargain is unavailable,
though a nonverbal gift or ritual may be attempted at Difficult (Easy if READ found the form).
### §0.3 — the hit-points pushback
**Accepted, with a caveat that matters.** Codex agrees HP must halve because the location pools
are gone, and that keeping ~26 HP with no locations would remove the danger. But it rejects the
framing that this is an *equivalent* trade:
> At HP 13 / Major Wound 7, a 1D20+5 variable sword causes a Major Wound on **95%** of hits and
> deals 13+ damage on **65%**. Against the original 26 general HP its base damage could never
> empty the pool in one hit, though it would devastate a location. v0.2 therefore converts
> frequent mutilation into frequent total incapacitation.
Ruling: keep the formula, do not sign off the combat balance until signature weapon damage,
armour values and the missing location-consequence table are tested together.
### Foundry schema failures (12 found)
Highlights: no character familiarity state anywhere; device modelling too coarse for
function-level analogy; Discovery has two competing authorities (`unlocks` is function-specific,
`scope` is tradition-wide) with no canonical matching rule; `holders` contradicts party
ownership and raw Actor UUID arrays break on import/duplicate; numeric `base` cannot express
`DEX×2` or `EDU×5`; projects omit difficulty, remaining hours, researcher caps and setback
links; the scrith material entry silently drops `deep_radar`, `hyperwave` and neutrino
absorption; `ALL` has no tag semantics; no weapon schema exists although materials expect
penetration tags; expedition data lacks leg records, explored status and multiple assets;
combat defence state has no defined owner document.
### Highest-value change for v0.3
> Make v0.3 a **complete vertical-slice playtest**, not a broad data release: one four-leg
> expedition with exact costs and hazards, leading to one native first contact, one three-step
> field research project, and one Builder device with function-level familiarity and Discovery
> data. Run that chain end to end **without GM invention or hand-edited Foundry state.** Fix
> every rule and schema that blocks it.
> "That single slice will test all four new subsystems — and reveal more than fifty pages of
> untested equipment data."
### Verification note
Ten of Codex's checkable factual claims have now been re-verified against the source corpus
across two rounds. **All ten held.** The only claim that initially failed to verify — library
research at 100 hours — turned out to be truncated by Claude's scratch deduplication script;
the un-deduped extraction confirms it.
---
## Round 3 — Codex review of RINGBRP_RULES_SPEC_v0.3.md
**Date:** 7 August 2026 · **Reviewer:** Codex (GPT-5.4) · **Author:** Claude (Opus 5)
### Arithmetic audit — Claude's published table was wrong in four places
Claude independently recomputed every P(Major Wound) with an exact-enumeration script
(`scratchpad/mw.py`) rather than trusting either party. Codex's recomputation and Claude's agree
exactly. **The v0.3 §3.4 table contained four errors.**
| Weapon | v0.3 published | Correct (MW 9) |
|---|---|---|
| Club / baseball bat | ~25% | **33.3%** |
| One-handed sword | ~30% | **37.5%** |
| Crossbow | ~25% | **18.8%** |
| Two-handed sword | ~60% | **76.0%** |
| Medium pistol | ~20% | **25.0%** |
Confirmed correct: punch 0%, dagger 8.3%, heavy pistol 40.0%, heavy rifle 58.3%, shotgun close
74.1%, hand beamer 50.0%, variable sword 85.0%, laser pistol/rifle 100%.
**The design conclusion survives** — the ladder still reads correctly from fists to lasers — but
the table must be republished with exact figures.
### Major Wound threshold — floor it at 9
Codex: `ceil(HP/3)` gives a light human (HP 23) a threshold of 8, and the step from 9 to 8
**triples** the dagger result (8.3% → 25.0%) and doubles several others. Verified independently.
Lower HP already makes small characters easier to drop; the extra wound cliff is unearned.
**Adopted: `MAJOR_WOUND = max(9, ceil(HP/3))`.**
Also: Claude's claim that 9 is "almost exactly BRP's absolute number" is **false** — BRP's is ≈7.
Nine is justified because it produces the right weapon ladder, not by BRP parity. Rationale text
must be corrected.
### Marksmanship Style — two dead rules and a trap
1. **"Reflex: first Dodge ignores the multiple-defence penalty" is a no-op.** The first defence in
a round never carries that penalty. Dead rule. If a benefit is intended, it must be *"the first
Dodge does not increment `defencesThisRound`."*
2. **"Deliberate: may aim across consecutive rounds, maximum +30%" is a no-op.** Deliberate
already reaches +30% from a single round of aiming. Dead rule.
3. **Gravity does not actually drive the choice.** Heavy-gravity characters roll DEX 2D6+6 *and*
POW 2D6+6 — both average 13. There is no mechanical pull toward Deliberate; the link is pure
flavour. Codex: make gravity **determine** the governing characteristic (light = DEX,
heavy = POW, normal = choice) rather than merely suggest it.
4. **Deliberate becomes a trap at high skill.** Codex modelled both styles at weapon 60 and found
a genuine trade (Reflex +31 moving/short; Deliberate +9 long, +7 aimed). But around weapon 90
the bonuses hit the 95% success ceiling, so Deliberate's advantages stop paying while its −20%
move-and-fire penalty persists — and style is permanent. Fix: give Deliberate something that
survives the ceiling, e.g. *a full round of aim treats long range as medium*, or *an aimed hit
improves one success level*.
### Major Wound consequence table — not ready
- Results 1–2 make **20% of all Major Wounds** merely "lose one action". Result 3 is nearly the
same. Too much of the table is nothing.
- Results 4–5 and 8 have the attacker choose arm or leg — that **is** location consequence, so
the "effects, not anatomy" claim is false. Assign 4 to manipulation impairment and 5 to
mobility impairment; remove the choice.
- "Hand unusable" and "two-handed actions at −30%" contradict each other — two-handed use should
be impossible.
- Disabled leg halves MOV but says nothing about Dodge, Athletics, falling or standing.
- Result 7 never says which skills, who picks the sense, or the treatment difficulty and time.
- Result 8's "permanent if untreated" conflicts with the Luck roll, which only governs
characteristic loss.
- Result 9 makes the GM invent the characteristic and never defines "out of the fight".
- **Repeated Stamina rolls have no stopping condition.** At CON 13 a character has only
`0.65³ = 27.5%` chance of still being conscious after three checks. Successful First Aid must
stop the sequence.
- §3.2 says the table fires "at most once per fight"; §3.5 says it fires on every Major Wound.
Contradiction with no state to back either.
- "Permanent" characteristic loss contradicts the later statement that an autodoc reverses it.
- Variable sword shifting only 8–10 → 10 yields amputation on ~28.5% of hits. If the intent is
the source weapon's mutilation rate, shift **4–10 → Catastrophic** (~66.5% of hits).
### Round-2 carry-over: 20 closed, 11 reopened
**Closed:** simweb trigger · library hours · mixed research results · new specialisation base ·
step difficulty · step cap · field objectives · maxResearchers · leg cost · Navigate vs Pilot ·
critical-on-unexplored · READ · Standing cap · OFFER gating · function-level analogy · holders ·
experienceEligible · sensor properties · `ALL` removal · chase procedure.
**Reopened / partial:** gravity enforcement and high-skill balance (`R-12b–d`) · Major Wound table
internals (`R-10`) · which resource failure consumes and who chooses (`R-31`) · recovery-fumble
target (§5 and §8 disagree) · two competing Integrity authorities · `unlocks` cannot resolve
because devices have no `deviceId` · `skill.base` formula grammar undefined · weapon schema
exists but is not buildable · **vacuum still has no effects or acquisition rule** · counter vs
immunity conflated with unrepresented precedence · species/pursuit/equipment/defect/vehicle
schemas **falsely marked closed — still absent**.
### Schema failures (worst)
No `deviceId`, so `device.cziltang_brone.operate` is a dangling reference · raw unlock paths need
string parsing and are not import-safe (use `{documentId, functionId}`) · derived HP and MW are
stored with no recalculation rule and will drift from CON/MAS · `activeWounds` can encode only
Bleeding · `rateOfFire: "1/3"` is an uninterpreted string with no round semantics, ammo cost or
reload · `shots` conflates capacity with current ammunition · shotgun has two competing damage
authorities · weapon `traditionId` singular vs device `traditionIds` array · armour lacks equipped
state, degradation and damage-type interaction · material `immuneTo` and weapon `penetrationTags`
are different namespaces with no matching rule · device functions lack governing skill, difficulty,
effect, duration and power cost · project steps are not a discriminated union · expedition legs
lack difficulty, duration, cost and table references · these are `system` fragments, not Foundry
documents — no `name`, `type`, `_id`, schema version, migrations or validation.
### Vertical slice — not buildable
> "No. The slice in §6 is a requirements list, not runnable content."
Missing: four actual leg records · the six-entry site and hazard tables · which resource a failure
consumes · Hanging People group statistics · their true taboo, desire, correct offer form and
rewards at Standing +1/+2/+3 · complete READ/SIGNAL/OFFER procedure (v0.3 has only amendment
summaries) · three complete research-step records · the brone's per-function skills, difficulties,
effects and power costs · Machine People raider statistics, ammunition, tactics and encounter
geometry · pregenerated explorers · firearm rate-of-fire and reload rules · executable wound and
treatment rules.
**Document-level contradiction:** the scope line says unchanged rules "carry over" from v0.2, but
the acceptance test permits *only this document*. A playtest spec must either contain the complete
procedures or formally import a versioned rules core.
### Verification note
Claude verified the arithmetic independently rather than accepting it. Codex's recomputation was
correct and Claude's published table was not. Across three rounds, every checkable Codex claim
that has been tested has held.
---
## Round 4 — Codex acceptance test of RINGBRP_v0.4_PLAYTEST_SLICE.md
**Date:** 8 August 2026 · **Reviewer:** Codex (GPT-5.4), running §15 as a GM · **Author:** Claude (Opus 5)
### Verdict
> **"FAIL. v0.4 is not runnable with zero GM invention."**
This is the correct and useful outcome of a first acceptance test. The rules survived three rounds
of review; they did not survive contact with actual content. That is what the slice was for.
Codex played the scenario start to finish, logging every point where it had to invent. It also ran
a 100,000-fight Monte Carlo model of the combat encounter.
### The three hard blockers
**1. The scenario gate is mathematically shut.** The control plate — the object the entire research
chain depends on — is a Standing **+3** reward (§11.4). But permanent Standing is capped at **±1
per encounter** (§6, a cap adopted from Codex's own round-3 advice), and the slice contains exactly
one contact. **+3 is unreachable.** Falling back to the random Site table, the plate requires a
critical Navigate on an unexplored leg followed by a 2 on 1D6:
> `1 − (1 − 0.02/6)² = ` **0.666%**
Two rules that were each individually correct combined into a scenario that cannot be completed.
**2. Combat cannot be resolved consistently.** There are **no general range modifiers anywhere in
the system** — so a pistol at 60 m fires at its full rating, directly contradicting §12.3's "their
weapons are useless at 60 m". Also missing: the hand beamer's range bands, the raiders' gravity
and marksmanship styles, movement distance while firing, DEX-tie resolution, a definition of
"down" for the morale trigger, and a minimum of zero on post-armour damage.
**3. The device is unusable even on success.** The brone is stored at Integrity 0. Research
unlocks `operate` but nothing repairs it — unlocking and repairing are different states with no
transition. Worse: complying with the raiders (an outcome §12.3 explicitly endorses) costs 3
Power, leaving 3, and `operate` costs 4.
### §12.4's Major Wound claim is false
The slice advertises that the heavy rifle exercises the Major Wound table at **58.3%**. Against the
party's actual armour:
| Target | P(Major Wound) per hit |
|---|---:|
| Unarmoured, MW 9 (the quoted figure) | 58.3% |
| Dunn / Tomas, armour 4 | **8.3%** |
| Ceyla / Rakhi, armour 6 | **0%** |
Fix: give the two rifle raiders Deliberate style and have them alternate aim and fire; their aimed
shots ignore 4 points of armour and would genuinely exercise the table.
### Combat model — neither style dominant, and Claude's suspicion confirmed
Sustained party output: Reflex pair 5.04 HP/round at entry, 6.44 at short range; Deliberate pair
6.22 HP/round. Deliberate is marginally better at entry, Reflex at close quarters.
> "Neither style is indispensable... If the beamer is ruled out of range, Reflex becomes clearly
> dominant. **The missing beamer range controls the conclusion.**"
Claude had independently flagged that the geometry might not test the styles as designed. Confirmed:
the pneumorifle's 200/400/600 bands mean the 60 m engagement is *short* range, where Deliberate's
+10% long-range bonus never triggers.
Monte Carlo (100,000 fights): party forces morale in 99.3–99.96% of runs, but casualty rate swings
from **14.5% to 81.2%** purely on whether the raiders focus fire or spread — an unstated NPC ruling,
not player tactics.
### Claude's three self-found defects — all confirmed by Codex
| Defect | Codex |
|---|---|
| §13.2 applies the facility modifier after the difficulty multiplier, contradicting §1 | Confirmed `[CONTRA]`. §1 gives 26%/20%, the worked example gives 16%/10% |
| §14 familiarity example says "5%" then "1%" | Confirmed `[CONTRA]` — "calls the result effective 5%, then says the floor applies, then says the floor does not apply, finally resolving it at 1%" |
| Physical category governed by STR, not DEX | Not raised by Codex, but stands — it makes Dodge, Athletics and Stealth strength-based, penalising the light-gravity pilot by 4 points |
### Pregens — verified sound
Codex independently recomputed every derived value for all four explorers. **All correct**, matching
Claude's own audit exactly. Skills judged "face-plausible" for the stated roles. Two caveats:
legality cannot be audited without an age/pursuit allocation procedure and a printed skill-base
catalogue; and Tomas carries a "minidoc" which §3.5's autodoc list (deskdoc/shipdoc/megadoc) does
not define.
### Research timing
Claude's 24.4% combined figure is arithmetically right *under the published (wrong) modifier order*,
but the calendar claim is not: expected **82 days**, not 60–80. Under §1's correct order it is
**49 days**. The cracked-plate penalty is catastrophic — a further −20 under difficulty-first
ordering drives both researchers to ~1%, giving **~1,005 expected research days**.
> "This is dead time, not play: identical rolls occur after 16–20 day gaps with no intervening
> decisions, threats or resource pressure."
Fix: replace the cracked penalty with a fixed 100-hour Average Repair step; make the 200 hours a
one-time investment with failed retries costing 40 hours and granting +10% cumulative calibration.
### Other confirmed contradictions
- **§1 fumble ordering.** The fumble test runs before the "96–99 always fails" clause, so at
effective values up to 80 some of 96–99 are fumbles rather than failures. Rewrite as: *"96–99
cannot be an ordinary success; they are failures unless the fumble range includes them."*
- **§5** Pilot may re-roll a failed Navigate "once per leg" without restricting it to vehicle legs —
Dunn can use Pilot (Flycycle) while walking.
- **§5** recovery fumble must damage a *different* track, but rolls 4–6 damage Integrity even when
Integrity was being repaired.
- **§§10, 11.4** contact happens *after* leg 3, so the leg-3 rewards are retroactive.
- Ceyla has no Athletics rating despite hazards and the aperture requiring it. Suggested 32%.
- Site 3 (Ghoul waystation) is a legal random result with no statistics, taboo, desire or rewards.
### Round-3 closure audit
17 of 22 closed. **Reopened:** result-8 permanence vs the universal Luck roll · full Foundry
documents ("incomplete JSONC specimens, not a creatable slice"). **Partial:** result-9 mapping for
multi-tag weapons · simultaneous wound sequences · the `majorWoundThisCombat` flag has no
mechanical purpose or reset · finite beam cells contradict "no reload".
### Required for v0.5
1. Move contact **before** the leg-3 roll; make immediate rewards depend on *impression* within the
encounter while permanent Standing still moves only ±1.
2. Add a general range modifier ladder: short +0, medium −10%, long −30%, extreme −50%, beyond
impossible. Retain the style +10%s. Movement with an offensive action = half MOV (15 m).
3. Give every weapon range bands in its own gear entry.
4. Give NPCs gravity origin and marksmanship style.
5. Make research completion set the brone's repair to Average at −20 familiarity; operation failure
costs 1 Power, not 4.
6. Fix the §1 modifier-order authority and apply it consistently to every worked example.
7. Physical category → DEX.
---
## Round 5 — Codex acceptance test of v0.5 · **FAIL**
**8 August 2026.** 32 of 47 closed · 14 partial · 1 open · **10 newly broken**.
**The decisive finding: v0.5's reachability claim was arithmetically false.**
Codex computed the informed contact route: READ 58 (special-or-better 12%), climb 48%,
best SIGNAL 29%, OFFER Easy 78%. Result: **P(Impression +2) = 1.697%, P(+3) = 0.174%.**
Modal outcome +1 at 55.1%. The random Site-2 fallback gave 0.499% (0.998% with a guide).
**The control plate remained gated.** Two versions had now failed on the same thing by
different routes.
Other confirmed defects: 30-item acceptance log including undefined critical/special attack
effects, incomplete action economy, cover that both penalised and blocked, pistol positions
contradicting the bounds route, no rifle aim cadence (without which the Major Wound table never
fires against armour 6), incomplete targeting ties, unpublished printed bases (so §1's 5% floor
could not be applied — and repair's true floor is 5%, not the 1% published), unpublished item
masses (so the eye-storm hazard could not resolve), and the post-compliance Power budget leaving
the brone inoperable after a diagnose.
Corrected maths Codex supplied and Claude accepted: research **93.5% by day 32, expected 25.9
days** (not "20–32"); step 3 expected 1.65 attempts; repair is a **reflecting walk to net
Integrity 3**, expected **8.48 attempts / 33.9 hours** (not "three successes, eight attempts").
Codex's combat simulation (20,000 fights/case): in cover shooting the rifles first — 50% chance
someone goes down, 0.2% TPK. Out of cover — 88%. Shooting the nearest raider instead of the
rifles — **48% TPK**. The tactical difference is enormous and was undocumented.
---
## Round 6 — v0.6 submitted
**8 August 2026.**
### The structural fix — `R-37`
> **A scenario-critical object must never sit behind a probability gate.**
> Rewards accelerate; they never gate.
The dead flyer is now a **fixed location**. Contact determines only speed and cost: +2/+3
immediate escort, +1 one-day detour, 0 three-day detour and −1 Supply. Contact keeps real weight
through a new reward — researching as the band's guests costs **no Supply**, against ~9 Supply
otherwise across a 45-day project.
### Also in v0.6
`R-38` project-hours not researcher-hours · `R-39` critical ignores armour, special halves it ·
`R-40` complete action economy (movement is free; cover, reload, aim and First Aid each consume
the action) · `R-41` cover penalises without blocking · `R-42` the immediate Stamina roll is the
first of three; unconsciousness 1D6×10 min · `R-43` targeting: nearest → lowest HP → lowest DEX,
and the down are not legal targets · `R-44` one OFFER per encounter · `R-45` margin defined ·
`R-46` climb failure · `R-47` diagnose costs 0 Power after the Discovery and reveals the exterior ·
`R-48` fumble Power cost · `R-49` net Integrity 3 · `R-50` abstract rations · `R-51`–`R-52` Ghoul
route completed · `R-53` complication 6 statted · `R-54`–`R-55` stationary Supply and hazards ·
printed bases and item masses published.
### Implementation
The ruleset now exists as a **working Foundry VTT system** at
`~/FoundryVTT/Data/systems/ringbrp` (v0.6.0), verified in a live Foundry v14 instance with zero
load warnings. Rules verified in-engine against the spec: fumble bands 98 at skill 60 and 96 at
20; 96–99 never succeed and 00 always fumbles; difficulty-before-situational (72 Difficult −20 =
16); HP and the floored Major Wound; the range ladder; critical/special/full armour; repair 5%
pre-Discovery and 38% post; diagnose 1%; aimed rifle 70% at 60 m.
---
## Round 6 — Codex review of v0.6 · **FAIL**, but the gate is closed
**8 August 2026.** Of the 30 round-5 acceptance items: **21 closed, 6 partial, 3 open.**
### R-37 worked — and then the same bug appeared twice more
> "R-37 closes the control-plate discovery gate. It does not remove every single point of failure
> from the critical path."
**1. The eye-storm could permanently delete the cziltang brone.** Rakhi's heaviest carried item
*is* the brone, at 12.5 kg — exactly what the hazard takes. Codex's arithmetic: 19.25% per
stationary hazard check, **57.5% cumulative across four**. The scenario would have died on a dice
roll more than half the time during the research phase.
**2. Power could be exhausted to a dead end.** The rim-wall site was not declared a Power-recovery
installation, and each failed operation costs 1 Power at a 48% failure rate.
**3. The "exactly enough, once" Power budget was false.** The now-guaranteed flyer yields 1D3
Power, so the post-compliance budget is **5–7**, not 4.
**4. The impression table was still wrong.** SIGNAL special (+1) plus OFFER special (+1) is **+2**,
not +3. Third time that arithmetic has been published incorrectly.
### Code-vs-spec divergences found (14)
Stale RollTable wording · pistol route contradicting the journal · Dodge base 0 vs DEX×2 · Ghoul
weapon mismatch · missing configured expedition/project/discovery · device fields absent from the
schema · rifle cadence and corrected pistol positions stranded in the Foundry journal and never
moved into the written spec.
---
## v0.6.1 — the response
`R-56` **THE CRITICAL-PATH INVARIANT**, promoted from an ad-hoc fix to a standing rule:
> *Nothing on the critical path may be destroyed, exhausted or made unreachable by a random
> result.*
- Hazards never take mission-critical equipment. Rakhi loses the toolkit, not the brone. Non-critical
losses are recoverable on a Spot the next day.
- The rim-wall site **is** a Power-recovery installation. Running dry costs days and Supply, never
the scenario.
- Power budget corrected to 5–7.
Plus `R-57` cell accounting · `R-58` the impression matrix, correct at last (+3 requires a
critical) · `R-59` rifle cadence and pistol positions moved into the spec · `R-60` the −3 archer
branch given a real procedure instead of a scope exemption · `R-61` scavenger theft procedure.
**Foundry synced to v0.6.1** and verified live: 9 compendia including a fully configured expedition
Actor carrying the three-step project, the discovery with its structural unlock reference, and the
contact group with its taboo; Dodge base as a formula; hazard tables rewritten; Ghoul hook.
### The pattern worth keeping
Three versions failed on the same class of defect in three different disguises — a gate, then a
destructible object, then an exhaustible resource. The lesson is not any of the individual fixes.
It is that **a critical path needs an invariant, not a patch**, and that only playing the thing
end to end reveals which disguise it is wearing this time.
---
# Round 7 — Strike Rank (Option 1) implementation + statline audit
*8 Aug 2026. Brief: `reviews/BRIEF_r7_strikerank_statlines.md`. Raw: `reviews/r7_codex_review.md`.*
Owner chose **Option 1, Deterministic Strike Rank**. Built, then reviewed. Codex returned
seven Part A findings and six Part B findings. Every claim below was checked against the
running system before being accepted or rejected.
## Verified true and FIXED this round
`R-57` **The expedition sheet was printing the answers to the first-contact challenge.**
The contacts tab rendered every taboo, every desire and `correctOfferForm` verbatim off the
embedded group item, while the expedition's own `knownTaboos` / `knownDesires` were empty and
`pidginEstablished` was false. The sheet displayed *"A gift of 0.5 kg of salt, passed hand to
hand, with both parties off the ground"* — the exact solution — to anyone who could open it.
Now: players see only what the expedition has actually learned, plus "Nothing learned about
this group yet."; the GM sees everything, marked GM ONLY with a yellow rule.
**This is the most severe defect found in seven rounds** — it silently defeats a scenario
pillar rather than producing a wrong number. Verified from both the GM and the player path.
`R-58` **The PC-before-NPC tie-break never fired.** It tested `actor.hasPlayerOwner`, which is
false for every pregen in a world with no players yet assigned — so all four PCs were being
treated as NPCs. Now tests `actor.type === "character"`. Verified: Ceyla (DEX 12) correctly
precedes the DEX 12 raiders she ties with.
`R-59` **Simultaneous combatants sorted alphabetically.** Replaced with insertion order, so the
result is stable and reproducible rather than an artefact of naming.
`R-60` **Strike Rank went stale, so P-10 did not actually work.** Nothing recomputed initiative
when load, weapon, skill or DEX changed, so "carrying loot changes your action economy
mid-scene" was inert. Now recomputes on actor/item change. Verified live: dropping the 12.5 kg
cziltang brone takes Rakhi from **SR 8 to SR 6** unaided, and picking it up restores it.
`R-61` **Training modifier applied to actions with no governing skill.** Move, Ready and Operate
Device were each taking the −30% "Untrained" +1. T is now only applied where the action has a
governing skill.
`R-62` **Melee defaulted to the equipped ranged weapon.** Rakhi declaring melee was scored on her
Pneumorifle. The default weapon is now filtered to match the declared action.
## Verified true, NOT yet fixed — these need an owner decision or are a separate build
| # | Finding | Status |
|---|---|---|
| A1 | `E` uses `ceil` per `R-23` "each 10 kg **or part**". Tomas is **1.0 kg** over his free carry and pays a full step of MOV, skill *and* SR. Codex recommends `floor` — rejecting all four of my proposed fixes — which drops the PC mean E from 1.5 to 0.5 and removes the cliff, at the cost of departing from the 1984 rule. | **Owner decision.** Changes `R-23`, which also governs MOV and skill penalties, not just initiative. |
| A6 | `T` spread of −2…+1 equals the entire beam-to-slow weapon spread, so 79→80% skill can outweigh weapon choice. Codex recommends −1/0/+1. Neither 1984 nor BRP:UGE coupled skill to action order; it is wholly our addition. | **Owner decision.** Changes agreed Option 1. |
| A2 | **The declared action is not enforced.** SR is computed from the declaration, but the attack controls never check it and nothing marks the offensive action spent. A combatant can declare Reload, take the Reload ordering, and then fire anyway. SR is currently advisory. | Needs action-spent state. Separate build. |
| A3 | **Aiming is free.** The attack dialog offers an unrestricted "Aiming" checkbox that grants the bonus and armour bypass with no earned aim state. Confirmed at `ringbrp.mjs:1269`. | Interlocks with A2. |
| A4 | `reloadRounds` is duration being spent as ordering. A 3-round reload delays within the round instead of occupying three. There is also no reload resolution that transfers reserve ammunition. | Separate build. |
| B1 | The Discovery gate is not enforced, and cannot be until acquisition is representable: the expedition already **owns** the discovery item whose text says "Not yet earned", so presence ≠ acquired. `unlocks` and `requiredDiscoveryIds` are inverse copies that can disagree. | Needs an `earned` state + one canonical authority. |
| B3 | `material.immuneTo` has no invocation point — the damage button rolls a formula with no target and no material, so it cannot know scrith was struck. Also: spec v0.3 says `penetrationTags`, the code says `damageTags`. | Needs target-aware damage. |
| B4 | Device status is derived as functional only at full Integrity, ignoring per-function `requiresIntegrity`. Needs per-function `locked / damaged / unpowered / ready`, plus a `modifiedByDiscoveryId` distinct from hard prerequisites. | Carried from round 2. |
| B6 | `group` has no shape in spec v0.3 at all — it was invented for the Foundry build. Needs structured `offers[]`, reward IDs, negative-standing consequences, language gating for SIGNAL, and GM-only ownership of the definition. | Design work. |
## Codex's ranked enforcement cut line for the statlines
| Priority | Fields | Treatment |
|---|---|---|
| **P0** | `requiredDiscoveryIds`/`unlocks`, `immuneTo`, `materialId` | Hard gate or immunity, explicit GM override |
| **P1** | `skillBonus`, `powerDraw`, `blocksSensors` | Apply and show in the roll; debit resources |
| **P2** | `slowTo`, `neutrinoAbsorption` | Warn only — neither has a quantified meaning, so automating would invent a rule |
| **GM reference** | `correctOfferForm`, `rewardsByStanding` | Never auto-adjudicate prose; hide unrevealed, prompt the GM |
## Correction to our own published work
`INITIATIVE_OPTIONS.md` stated `E = floor(...)` and that Rakhi was the only encumbered
character. Both were wrong: the rule is `ceil`, and all four explorers are over their free
carry. The worked example has been replaced with the order the built system actually produces,
verified in Foundry 14.361. Codex also correctly noted the round-7 brief itself repeated the
stale `floor` — the brief was written from the doc, not the code.
**Scoreboard.** Round 7 is the first round where the most serious finding was an information
leak rather than an arithmetic error, and the first where a defect (`R-57`) would have been
invisible in play until it had already spoiled a session.
---
# Round 8 — encumbrance rework, readiness, dice, playthrough
*8 Aug 2026. Brief: `reviews/BRIEF_r8_encumbrance_readiness.md`. Codex round 8 was
still running at the time of writing; findings will be folded in when it returns.*
Owner directives implemented, then tested by play rather than by inspection.
`R-23` **revised.** Encumbrance now has two readings of one condition, deliberately:
`floor(over/10)` whole steps drive MOV and skill penalties as before, while a
continuous `ceil(over)/10` drives Strike Rank. Any overage at all now costs part of
a rank and no single kilogramme costs a whole one. The 1 kg cliff is gone.
`R-69` **Initiative is no longer only about weapons.** The single skill term split
into two, each narrow: **T** training, the declared action's own skill, and **R**
readiness, the best of Tactics, Insight, Spot and Listen, floored by INT × 3 so a
clever but untrained character is not flat. New **Tactics** skill, Mental, base 5.
`R-70` **The bands were tuned to a distribution that does not exist.** After
narrowing to −1/0/+1 the thresholds sat at 80 and 30 — but measured across all 102
skill ratings in the world, weapon skills span **31–59** and top out at 59, so the
−1 band was literally unreachable and both T and R were flat +0 for every character.
Retuned to **55 / 35**, the observed quartiles. Skill now actually moves initiative.
*Found by measuring, not by reading.*
`R-63`, `R-64` the two-axis modifier system and the ±40% two-modifier cap.
`R-65`–`R-68` defects found by playing: new characters started wounded, "Automatic"
difficulty rolled normally, aiming is free for rapid weapons, packs need a restart.
All detailed in `RULES_COVERAGE_AUDIT.md`.
**Dice.** Three house sets registered with Dice So Nice — Void (flat), Shadow
Squares and The Arch — in the sheet palette with Michroma numerals, plus matching
in-chat tens/units chips that work whether or not 3D dice are enabled.
One real integration bug: `addTexture` returns a **Promise**, so colorsets
registered in the same tick silently lost their textures. Every call is now awaited.
**Scoreboard.** Round 8's most valuable finding came from neither AI reviewing code
nor from the specs, but from measuring the actual data (`R-70`) and from creating a
character and playing it (`R-65`, `R-66`, `R-67`).
# Round 9 follow-up — blockers closed, findings 5/6 and R-56 fixed
*8 Aug 2026. Codex returned FAIL on round 9 with two blockers. Both were correct.*
`R-71` **The setting reproduced Fringeworthy's premise.** Under-ice gateway, ancient
builders, alternate Earths, and a rare compatible minority named for the fringe —
Codex was right that the combination could not read as coincidence, and that
"fringe-capable" was "Fringeworthy expanded into an adjective". Replaced with
Codex's own suggestion, which is a better idea than the original: **anyone can
cross, crossing is an induced misidentification, and Coherence is the accumulating
error.** No door, no builders, no network, no catalogue, no gifted minority. The
mechanic is now the reason travel works rather than a consequence of it.
`R-72` **Both species were reskins.** Triune retained the entire Puppeteer design
and Ferren was still recognisably a Kzin. Replaced from the silhouette up: the
**Vesh** is bilaterally asymmetric with no head at all (senses on a dorsal ridge,
so no killing headshot and no face to read), and the **Cadence** is four to six
bodies that are one person, so the location roll picks which body is hit and
killing one reduces rather than kills.
`R-73` **Displacement was prose only** — zero clamped and sat there, and recovery
would walk a displaced character back to 1. Now an event: resets to full, archives
the drift log, whispers the GM to change one true thing.
`R-74` **"−X% everything" affected almost nothing** — Coherence was applied in the
skill-tab handler only, so attacks, characteristic rolls, Luck and case phases all
bypassed it. Moved to the single choke point in `skillRoll`.
`R-75` **Coherence was entirely manual.** Now charged by the system: a fumble while
across, and any use of transposed materiel, both fire automatically; `debrief()` is
one operation and refuses to run while the team is across. Verified: 8 → 7 on a
fumble, 7 → 6 on the lance, blocked across, 6 → 7 at home.
`R-76` **The case-file remap was naming only.** The phase resolver still chose a
"navigator" by Navigate; it now leads on **Tradecraft** with Navigate as fallback.
`RINGBRP.caseTracks` maps the stored fields to case vocabulary — supply→Resources,
power→Clearance, integrity→Containment — without breaking existing actors.
`R-77` **R-56 violation confirmed and closed.** "Cannot open a path home unaided"
was a gate whenever no second agent existed. The way home is now never closed:
aided 0%, alone −30%, and a failed return lands you *near* the target rather than
nowhere. Verified: `possible=true` in both cases.
# Playthrough — The Ilfracombe Crossing, 8 Aug 2026
*Ran a whole case in the live world: import, four phases, a firefight, the device,
the way home, debrief. Nine defects, every one found by playing rather than reading.*
`R-78` **The case file shipped with no team.** `members: []`, so the phase resolver
had nobody to lead and Debrief iterated an empty list. The published case was
unusable on import. Now built with its four agents.
`R-79` **Two consumers, two shapes.** The resolver read `members` as bare ids; the
Debrief handler I wrote read them as objects. Standardised on ids.
`R-80` **Bulk find/replace corrupted the language file.** The setting rename ran
`" leg" → " phase"` and turned hit locations into **"Right phase"**, `LocEffect.leg`
into "The phase gives way", and a chat title into "case team phase". Seven strings
repaired. The lesson is the one from the earlier case-sensitivity miss: a rename
sweep needs an audit pass with *different* patterns from the ones that made it.
`R-81` **Two of the three case tracks were inert.** Across a full four-phase case,
Clearance and Containment never moved once — only Resources was driven by anything.
Failure now costs Containment, a fumble costs Containment 2 and Clearance 1.
`R-82` **R-56 violated at case scale.** An exhausted track set `inCrisis` and the
sheet then *blocked* phase resolution — a case that ran out of Resources could not
continue at all. Now it degrades: phases go Difficult and cost Containment, and can
always be attempted. Verified with Resources 0 and Clearance 0: phase resolved,
`blocked=false`.
`R-83` **Forced encounters were data nobody read.** Both dramatic beats of the case
— the contact and the ambush — were declared in the phase data and referenced by
zero lines of code. Now posted to chat when the phase resolves.
`R-84` **The phase lead took the best of Tradecraft or Navigate** rather than
preferring Tradecraft, so Dunn's Navigate 67 beat the Tradecraft it was meant to
showcase. Now Tradecraft first, Navigate only as fallback.
`R-85` **The device gate was never enforced** — Codex flagged this in rounds 7 and
9 and it was still true. `requiredDiscoveryIds` was read by no code, so the locked
function at the centre of the case could be operated on turn one. Now enforced with
four distinct refusals: locked, damaged, unpowered, ready.
`R-86` **Presence could not mean acquired.** The discovery item shipped *on* the
case file, so "does the team have it" was true from the start. Discoveries now ship
`earned: false` and `earnDiscovery()` is the moment the case turns.
**Scoreboard.** Playing found nine defects in one session, including two that made
published content unusable (`R-78`, `R-85`) and one invariant violation (`R-82`)
that the two AI review rounds had both missed at case scale despite catching its
character-scale twin.
# Second playthrough — BRACKWATER, 8 Aug 2026
*A deliberately different case: a Hollow world, unexplored phases, the tables, first
contact, and the new species anatomies. Four more defects.*
`R-87` **First contact had no engine at all.** Standings, impression, taboos,
desires and `pidginEstablished` were all stored, the three-stage READ/SIGNAL/OFFER
procedure was written up in the slice — and **zero code** touched any of it. The
entire social pillar was prose over inert data.
Worse, it stranded `R-57`. That fix hides a group's secrets until the team has
*learned* them, reading from `knownTaboos` — and nothing in the system could ever
write to `knownTaboos`. Players would have seen "Nothing learned about this group
yet" for the whole campaign, permanently. A privacy fix and a missing feature that
were individually invisible and together made the content unusable.
Now implemented: three stages with their own skill pools, READ writing learned
taboos and desires back to the standing, SIGNAL establishing pidgin, OFFER the only
stage that moves standing, and `R-44` (one OFFER per encounter) enforced. Verified:
READ learned the taboo and the desire, impression reached 2, a second OFFER in the
same encounter was refused and allowed in the next — and the player view then
correctly showed exactly the one taboo and one desire the team had earned.
`R-88` **A re-attempted phase re-fires its forced encounter.** A fumbled phase does
not advance, so the same forced encounter posts again. Left as-is for now — noted
rather than fixed, because "the thing you failed to get past is still there" is
arguably correct.
`R-89` **Setting content survived in the group and slice prose.** The desire was
still "Salt. None in the canopy; the nearest seep is eleven days away", the site was
a "dead flyer", and one heading read "the The Quiet Neighbours" from an earlier
substitution. Four sweeps had passed because each used the patterns of the previous
rename rather than looking for what the *content* was still about.
`R-90` **Cadence anatomy verified in play**: 26 hit points derived into five bodies
of 6, all `unit` kind, no vital and no head, location rolls distributing evenly.
**Scoreboard.** Two playthroughs, thirteen defects. The pattern across both is the
same: the mechanics that were *reviewed* held up, and the ones that were only ever
*written down* did not exist. `R-85` and `R-87` are both cases where a document
described a system in detail and no code implemented any of it.
# Third playthrough — wounds, displacement, healing. 8 Aug 2026
`R-91` **Armour was skipped on one of the two damage paths.** `resolveHit` applied
armour; `hitLocationRoll` did not, so the same blow did more damage when it went
through the location roll — the path the sheet button uses. Now applies worn armour
plus any location armour, with the critical bypass preserved. Verified: 7 damage
against armour 4 gives 3 through on a success and 7 on a critical.
`R-92` **Healing existed nowhere and now exists.** Flagged in the coverage audit and
twice more in play without being fixed: a wounded agent could not recover by any
means the system offered. Implemented BRP-standard natural healing (1 per week),
First Aid (3, +20% with a kit, −1 on a fumble, once per wound), the field medic pack
(1 per 4 hours) and the trauma unit (1 per 2 hours). Location damage mends alongside
hit points. Characteristic loss is still not reversed, which is the standing rule.
Verified: Dunn at 5/23 → First Aid +3 → 14 days natural +2 → 12 hours trauma unit
+6 → 16/23.
`R-93` **CLOSED — location mending was silently dropping full heals.** In the same run,
a 2-point heal correctly mended `legR` from 3 to 1, but a subsequent 6-point heal
mended nothing at all despite 4 points of outstanding location damage. The mend loop
in `heal()` was correct; the write was not. **Foundry MERGES object updates**, so
`delete dmg[id]` on a cloned object leaves the stored value untouched — a location
mended to zero silently kept its damage, while a partial mend appeared to work
because it was a decrement rather than a deletion. My guess that
`prepareDerivedData` was rebuilding the object was wrong: that code only reads it.
Fixed by writing per-key paths and removing cleared locations with Foundry's `-=`
deletion form. Verified against the exact sequence that failed: 6 trauma-unit points
now clear `{legR:1, armL:3}` to `{}`, a full heal empties the object, and a targeted
mend clears the named location first.
Corrections to my own earlier reporting: `resolveLocationHit` *does* return
`majorWound`, which I initially read as a missing check — the flag was there and my
test harness simply did not print it.
**Scoreboard.** Three playthroughs, sixteen defects. Two subsystems described in
detail by the design documents turned out to have no implementation at all (the
device gate, first contact), and one core rule had never been written in either
place (healing).
# Fourth playthrough — the end-of-case loop. 8 Aug 2026
`R-94` **Experience was stored and never earned.** Every skill carried
`experienceTicked` and `experienceEligible`, the sheet drew the tick, and nothing in
the system ever set one — so the flags were decorative and no character could ever
improve. A successful roll now ticks its skill automatically, and `endOfCase()` runs
the BRP improvement roll on every ticked skill: roll over the current rating to gain
1D6, ticks clear either way. Verified in play — two skills ticked from actual rolls,
both rolled under and gained nothing, ticks cleared, and a second call correctly
does nothing.
This is the step that closes the episodic loop the owner asked for: a case ends, the
agents are a little better, and the ticks reset for the next one.
`R-95` **CLOSED — gear bonuses were prose.** The cover identity pack promised "+20% to Fast
Talk" and the medical kit "+20% to First Aid" in their descriptions, while
`skillBonus` shipped as `{skillFamilyId:"", value:0}` on every item and was read by
no code. Now populated at build time AND read: `gearBonusFor()` applies the best matching
carried item at the `skillRoll` choke point, so every path gets it. Only the single
best item applies — a pack full of medical kits does not make anyone a surgeon — and
dropped gear stops helping immediately.
`R-97` **The medical kit had two authorities for one number.** `firstAid()`
hardcoded +20% while the item itself carried `skillBonus: +20% first_aid`, so once
the reader existed the bonus would have applied twice. The hardcoded copy is gone
and the item is now the only source. Verified: First Aid 78% → 98%, with two kits
still 98%, and 78% with the kit dropped.
`R-96` **CLOSED — NPC targeting was prose only.** "Fires at the nearest visible
agent; ties go to the lowest current hit points" was written into three statblocks
and lived nowhere else, so every GM arbitrated it by hand and inconsistently.
`npcTarget()` now resolves it in the published order — nearest visible, then lowest
current hit points, then lowest DEX, then the GM chooses from what remains — and a
DOWN character is not a legal target while anything is still standing, because
these enemies are being paid rather than fanatical and shooting the unconscious
wastes a round. `declareNpcTarget()` whispers the choice and the reason to the GM,
so the table can see the rule was followed rather than fudged.
`npcShouldWithdraw()` implements the morale line from the same statblocks — "when
two of a team of six are down, the rest withdraw" — as a fraction, so it holds for
any group size.
Verified across all eight branches: nearest wins; a range tie falls to hit points;
a hit-point tie falls to DEX; a full tie reports GM choice and returns the tied set;
downed targets are skipped while others stand; the downed become legal only when
nobody stands; hidden tokens are ignored; an empty field returns no legal target.
Morale fires at 2 of 6 and not at 1 of 6.
**Scoreboard.** Four playthroughs, nineteen defects. Every one of the last three
sessions found at least one subsystem that was fully described in the design
documents and entirely absent from the code.
# R-98 — the three "unloadable" fields, root cause. 8 Aug 2026
Not a Foundry bug, not a LevelDB bug, not a data-model bug. **Foundry was never
being stopped.**
Every restart this session used `pgrep -f "main.js --dataPath=..."` to find and
kill the server. That pattern matched the *first* instance, which ran as a bare
`node main.js`. After the app was relaunched with `open -a`, it ran as the bundled
Electron binary with entirely different process arguments — so from that point on
`pgrep` matched **nothing**, `kill` was a no-op, and Foundry kept running.
The check for "did it stop" used the same pattern, so an empty result was read as
"stopped" when it actually meant "not found". A verification that can only confirm
what it is looking for is not a verification.
Everything else follows: Foundry held its original in-memory compendium for hours,
never re-read the rebuilt packs, and served a `MARKER` value that survived deleting
the entire packs directory — because the process that held it never died. The disk
was correct the whole time. So were the build script, the data model and the packs.
Two false diagnoses were published along the way and are recorded here so they are
not re-guessed: that `prepareDerivedData` was rebuilding `locationDamage` (it only
reads it), and that concurrent read-write LevelDB opens were corrupting the packs
(a genuine hazard, but not this).
Correct way to stop Foundry, whatever it was launched as — find it by the port it
is actually serving:
PID=$(lsof -nP -iTCP:30000 -sTCP:LISTEN -t | head -1)
kill -TERM $PID
# then confirm the PORT is free, not that a name pattern is absent
Verified after a genuine stop, clean rebuild and fresh start: marker gone, gear
bonuses present, case file members 4, discovery `earned: false`, and the full path
from compendium to actor to roll works — First Aid 78% → 98% with the kit.
# Fifth playthrough — the import path, now that packs actually load. 8 Aug 2026
`R-99` **The shipped case file resolved to nobody.** `R-78` stored `members` as raw
actor ids taken from the compendium. Foundry assigns fresh ids on import, so after
a normal import the case file reported four members and resolved **none** of them —
the phase resolver had no lead and Debrief iterated an empty list. The count looked
right, which is why it survived the earlier playthrough: I checked `members.length`
and never checked that the ids pointed at anything.
This was only findable once the pack-loading problem (`R-98`) was fixed, because
until then no import reflected the built content at all.
Fixed by storing **stable keys** rather than ids: every pregen is stamped
`flags.ringbrp.key`, the case file stores those keys, and `caseMembers()` resolves
key → flag → id → name, tolerating all three shapes `members` has had over the
project's life.
A second defect inside the fix: the flag was initially written before `...BASE()`,
which spreads an empty `flags: {}` and silently overwrote it. Order matters in
object spread and the symptom was an undefined flag with no error.
Verified from a clean import: members resolve to all four agents, four phases run
with all three tracks moving (a phase-4 fumble took Containment 2 and Clearance 1),
contact resolves, the device gate correctly reports locked, and debrief recovers
Coherence for the whole team.
**Scoreboard.** Five playthroughs, twenty defects. `R-99` is the third case of a
fix that made the *symptom* look correct while leaving the behaviour broken —
alongside `R-95` (data populated, no reader) and `R-57`/`R-87` (secrets hidden, no
way to learn them). Checking the number rather than the behaviour is now the
single most reliable way I have produced a false pass.
# Sixth playthrough — the sheets as UI. 8 Aug 2026
*Everything to date had been driven through the API. This drove the actual DOM.*
`R-100` **The Resolve Phase button was permanently disabled.** The case-file sheet
computed its own phase lead — resolving `members` by raw id and picking on Navigate
— duplicating logic that `resolveLegDialog` had already been fixed to do via
`caseMembers()` and Tradecraft. So the API path worked perfectly while the button a
GM would actually click was dead, and the sheet reported `navigator: NONE`,
`blocked: true`.
Two copies of one lookup, one of them fixed. The sheet now calls the shared
resolver. Verified by clicking: `Ceyla Marspont 55% (tradecraft)`, rollTarget 27,
button enabled, phase 0 → 1, two chat cards, tracks moved.
Fixing it exposed two dangling references to the removed local (`ctx.blocked` and
`ctx.rollTarget`), both of which threw at render — caught immediately because the
sheet failed to open, which is the useful kind of breakage.
## What the UI pass confirmed working
Character, NPC, case-file, weapon, gear and skill sheets all render with no console
errors. 45 rollable elements on the character sheet. Clicking a characteristic
opens the roll dialog and posts to chat. The Coherence buttons move the track
(8 → 7). Tab switching works across all six tabs. On the case file: Across/Home
toggles, the six track-adjust buttons work, and Resolve Phase now runs.
## Two false alarms, both mine
I reported tab switching as broken; it was fine — I had several sheets open and was
reading `.active` from the wrong one. I reported the NPC sheet as failing to render;
it renders fine, my harness mishandled the element reference.
**Scoreboard.** Six playthroughs, twenty-one defects. `R-100` is the fourth
duplicated-logic defect (`R-97` medical kit, `R-84` phase lead, `R-76` case tracks)
— every one the same shape: a rule implemented twice, fixed once. Worth a standing
rule of its own: when fixing a lookup, grep for every other place that does it.
# Seventh playthrough — the journal and the tables. 9 Aug 2026
`R-101` **Old-setting vocabulary survived in the journal page titles.** "The Journey
— four legs" and "Combat — the defile" — plus body text — came through every
previous sweep because those sweeps searched for *proper nouns* (Kelnok, cziltang,
scrith) and the surviving words were ordinary ones the conversion had quietly
retired: "legs" for phases, "defile" for the terrain. Renamed to "The Approach —
four phases" and "Combat — the cliff path".
A capitalised-variant miss inside the same fix: the first pass caught `defile` and
`four legs` but not `Defile` and `Four legs` at the start of a heading. That is the
same case-sensitivity error made earlier in the project, in the same file.
284 documents now clean against a pattern including the ordinary-word retirees.
`R-102` **OPEN — table effects are prose.** Drawing from the hazard table returns
its text and changes nothing: no track is debited, no roll is prompted, and there
is no table-effect function anywhere in the API. Confirmed by drawing a hazard and
watching Resources/Clearance/Containment sit unchanged at 6/6/9. Same shape as the
device gate and first contact before them — a mechanic fully specified in content
and implemented nowhere. **Not attempted; no code written.**
## Duplicated-lookup audit
Acting on the standing rule from `R-100`, I grepped for every remaining duplicate of
the lookups that have caused four defects. All clean: `system.members` is now only
read through `caseMembers()`, and coherence and gear bonuses are each applied
exactly once, at the `skillRoll` choke point.
**Scoreboard.** Seven playthroughs, twenty-two defects. The pattern holds: reviewing
finds errors in code that exists; playing finds code that does not exist at all.
# Round 10 — first playable build, and Codex as an adversarial GM. 9 Aug 2026
Built: 18 talents in four categories with a Talents tab; equipment to 27 gear, 18
weapons, 10 armour; **76 custom flat SVG icons** on every item and actor, replacing
all stock Foundry art; icons wired through the skills, combat, gear and talents
lists. Verified in the running world: 27/27 items on a pregen carry custom art.
Codex reviewed it as a cooperative-but-adversarial GM and the verdict is harsh and
largely correct.
`R-103` **My "284 documents clean" claim in `R-101` was false.** The scenario's
opening was still, almost verbatim, the Ringworld case: "two falans", "four hundred
thousand kilometres to antispinward", "starboard rim wall", "spaceport ledge",
"Architect freeway". Every sweep I ran passed because every sweep searched a word
list built from the *previous* sweep — and none of those words was ever on it. The
README still read "Basic Roleplaying on the Great Arch" and so did the engine's own
first line.
This is the fourth time a rename sweep of mine has passed its own check and failed
someone else's, and the first time I published a false all-clear on the strength of
it. **A sweep that greps for the terms you already thought of cannot tell you
whether you thought of enough terms.**
Fixed: the case opening is now an actual Custodians brief — a Custodian team missing
eleven days, a beacon still transmitting, something holding a crossing open that
should not be able to. 329 documents now clean against a term list that includes
the megastructure vocabulary I had never checked for.
`R-104` **Kit and training bonuses were silently colliding.** `gearBonusFor()`
iterated *every* embedded item, so talents were swept into the gear pool and the
"best single item applies" guard then discarded one of them — a medical kit and
Triage collapsed into a single +20 rather than +40. Now two separate pools that
stack, with best-of-one *within* each. Verified: Tomas 78% → 118%.
## Codex's findings not yet fixed — all confirmed by inspection
| | |
|---|---|
| **The talents are cosmetic.** No runtime reads `grantsAction`, `usesPerCase`, `usedThisCase` or talent `coherenceCost`. There is no Use control. Only the numeric `skillBonus` does anything, and only by the accident fixed above. | Large build |
| **First contact has no UI.** `resolveContact()` is API-only; the case sheet has no READ / SIGNAL / OFFER buttons, so the scenario's forced contact stalls the session dead. | High |
| **The case cannot be completed through the UI.** No resolve-step control for research projects, no Operate/Diagnose/Repair buttons, no way to earn the Discovery except from the console. | High |
| **Group rewards are inert arrays** — impression moves, nothing it promises happens. | High |
| **The journal contradicts the engine** — it says Navigate governs phases; the engine prefers Tradecraft. | Medium |
| **Ceyla has Cover Work but no Fast Talk skill**, so her signature talent cannot fire. | Medium |
| Research as written is ~44 days plus 34 hours of repair — not one session without a montage procedure that does not exist. | Design |
**Scoreboard.** Ten rounds, 104 findings. The pattern of this round: everything
*visible* was built and works — sheets, icons, talents on the page — and almost
nothing behind the newest content is wired to a button. Codex's single most useful
sentence: *"the exact first-session stall is 'The Quiet Neighbours intercept you.
Now what button do I press?' There is none."*
# Round 10b — contact and device controls wired. 9 Aug 2026
*Answering Codex's two highest-severity findings: the first-session stall and the
uncompletable case.*
`R-105` **First contact now has controls.** The case-file contacts tab carries
READ / SIGNAL / OFFER / New encounter per group, with live impression, standing,
pidgin and offer-spent state. Choosing who talks opens a picker **ordered by who is
actually best at that stage, with the rating shown**, so the obvious choice is the
default and the interesting choice is still one click away. Verified end to end:
picker listed Tomas 58% (insight) above Ceyla 46% above Dunn 25%, and READ resolved
to chat. OFFER greys out once spent and New encounter restores it.
`R-106` **Device functions now have buttons.** Operate / Diagnose / Repair appear
under any device on the gear tab, each showing its own live blocker rather than a
generic disabled state — "Locked — the discovery that explains this has not been
made", "Too damaged. Needs Integrity 3; it has 0", "Not enough Clearance. Needs 4;
the case has 2". `useDeviceFunction()` resolves the roll, applies familiarity after
the Discovery, charges Clearance and Coherence, moves Integrity on repair, and
posts the function's own printed outcome text.
Verified as a chain: Integrity 3 alone stays locked → earning the Discovery unlocks
it → Operate succeeds, charges **4 Clearance and 1 Coherence**, and dropping
Clearance to 2 re-blocks it with the reason stated.
`R-107` **A cost of 4 was being charged as 0.** `powerCostAfterDiscovery` is absent
from the source data but the data model fills absent keys with **null**, so the
guard `f.powerCostAfterDiscovery !== undefined` passed and `Number(null)` gave 0 —
Operate ran free. Now `!= null`. Caught only because I checked the track moved
rather than that the button worked, which is the rule from `R-99` doing its job.
Still open from round 10: the talents remain mechanically inert (no Use control, no
reader for `grantsAction` / `usesPerCase` / talent `coherenceCost`), group rewards
are still inert arrays, research projects have no resolve-step control, and the
journal still says Navigate where the engine prefers Tradecraft.
# Round 10c — talents made mechanical. 9 Aug 2026
*Codex round 10 finding 4: "a cosmetic talent release, not a mechanical one."*
`R-108` **Talents now do things.** The split that makes this work is ACTIVE versus
PASSIVE, decided by the data rather than by hand: a talent is active if it costs
Coherence, is limited per case, or bends an action — those get a Use button.
Everything else is passive, has no button, and is read at the point it applies.
That prevents the failure mode where a sheet is covered in buttons that mostly say
"yes, you still have this talent".
Active, verified: **Echo Sense** charges 1 Coherence (10 → 9). **Clean Scene** is
1/case, arms, refuses a second use, and its flag clears when spent. **Requisition**
trades 1 Clearance for 2 Resources or the reverse, refusing when neither track can
pay.
Passive, verified: **Chain of Custody** absorbs a transposed cost entirely (a
1-point crossing key now costs Rakhi nothing). **Triage** adds a point to First Aid
(Ceyla 5 → 9 where a plain success gives 3). **Read the Room** learns a second fact
on a successful READ (Tomas learned 2 in one roll). **Debriefer** gives another
agent an extra point at debrief. **First Through** and **Out of Step** bend Strike
Rank to 1 and by −1 respectively.
`R-109` **`endOfCase` reset talents after its early return**, so a character who
ticked no skills kept every talent spent — permanently, for the rest of a campaign.
Moved above the return: end of case is end of case whether or not anything was
learned. Verified: clean_scene 1/1 → 0/1 with zero ticked skills.
This also answers Codex's `Chain of Custody` concern in the affirmative — it *does*
reduce a 1-cost item to free, which removes the central tension for that one agent.
Flagged for the owner rather than silently nerfed; the talent is doing exactly what
it says, and whether what it says is right is a design call.
Still open: group rewards remain inert arrays, research projects have no
resolve-step control, and the journal still says Navigate where the engine prefers
Tradecraft.
# Round 11 — generators, and Codex's verdict on them. 9 Aug 2026
*Owner: "press a button and a character is created", with full customisation
still available, plus NPCs, monsters and things in between.*
Built `generateCharacter()` and `generateNPC()`, both callable with **no arguments
at all**; a dialog that opens pre-randomised so the fastest path is open → Create;
buttons on the Actors sidebar; 5 roles, 4 experience tiers, 6 NPC threat tiers,
3 species, and a count field for 1-12 at once.
Codex reviewed as an adversarial GM. Verdict: *"Do not ship this as fully formed
character generation. It is a competent statblock seeder."* It was right.
`R-110` **Most combat NPCs could not use their own weapons.** The threat tier chose
the weapon while an unrelated random PC role chose the skills, so the two never had
to agree. Codex's measured numbers: Trained received no weapon at all, Armed 44%,
Dangerous 20%, **Apex 0%** — eight Apexes in ten had no usable attack. Replaced
`weapons: [...]` with `attacks: [{weapon, skill}]`, granted as one invariant.
Verified 10 of each tier: **60/60 weapons now have their skill**.
`R-111` **"Make four" produced four copies of one character.** The dialog resolved
"random" ONCE and passed the same role to every actor in the loop, with species and
tier hard-defaulted. Now every choice resolves per actor, "Random (rolled
separately for each)" is an explicit option, and party mode guarantees role
coverage when several PCs are made with the role left random. Verified: a party of
four came out with three distinct top skills instead of four identical sheets.
`R-112` **The generator was rolling characteristics that do not exist.** It rolled
`app` and `mas`; the data model has `str siz con int pow dex cha edu`. The Vesh
`app: -2` shift was therefore modifying a dead field — species generation was
mechanically lying. Also wrote `origin.homeWorld` where the sheet reads
`origin.world`, so every generated character had a blank home world.
`R-113` **Two shipped weapons had no skill to roll.** The Collapsible baton needs
`melee_weapon:club` and the Error cutter needs `transposed_weapon`; neither existed
in the 50-skill catalogue. This was a **content bug affecting players**, not just
the generator — anyone handed a baton could not attack with it. Catalogue now 52.
`R-114` **The Cover Work class of bug, reproduced and then closed.** A generated
talent whose `skillBonus` names a skill the character was never granted is dead on
arrival. The generator now grants that skill. Verified across all five roles: zero
dead talents, zero unusable weapons. Also added: everyone carries a **personal
anchor** (the character hook and the only Coherence recovery route) and an agency
handset, reserve ammunition is stocked at twice capacity, and the Liaison's
duplicated Trade Cant is deduplicated.
Still open from Codex round 11: Containment starts encumbered by its own issued
kit; the bonus-point spray can produce one absurd skill; blank biography, fixed age
30 and identical portraits; and the gap between Armed and Anomalous where most
Custodians cases actually live.
# Round 11b — encumbrance, biography and portraits. 9 Aug 2026
`R-115` **Everyone is issued a full kit; nobody carries all of it.** Containment
began every case at +0.78 SR for owning its own breaching charges. `stowOverload()`
now marks the heaviest items `carried: false` after kitting, so the kit is *issued*
and the player decides what comes off the vehicle. Containment is now +0.00 to
+0.34 depending on the STR roll, and every other role is exactly 0.
Two flaws in the first version of that logic, both found by reading the output
rather than trusting it:
- It stowed **everything** when it could never get under, because heavy armour is
never stowable — including shedding a 0.1 kg keepsake to fix a 4 kg overload.
It now only stows an item when that item is big enough to matter, and gives up
gracefully instead of stripping the character.
- The **personal anchor was being stowed**. It is the character hook and the only
Coherence recovery route, and it weighs 0.1 kg. It is now never stowable, and a
secondary melee weapon is, because an entry bar can be slung.
The role's key characteristic is now the best of three rolls rather than two, so a
Containment specialist is actually strong enough to carry what Containment is
issued. The residual load on a low-STR Containment agent is the 11 kg breaching
suit, which *should* slow you down and is left alone deliberately.
`R-116` **Biographies and portraits.** Every generated character gets a short
service record — role and tier, an origin drawn from their home gravity, how they
were recruited, a role-specific observation, and an **anchor**, which ties the
character hook to the Coherence rule rather than leaving it as colour. Ages are
26-54 instead of a fixed 30. 25 new portrait icons: ten baseline, five Vesh (mask),
five Cadence (three-body), five threat. Verified 4/4 distinct across a party, and
species-appropriate.
A cluster of three Physicians in one sample of four looked like a distribution bug.
It was not — over 24 characters all five roles appeared 2-7 times and all six
anchors 2-6 times. Checked rather than assumed, and left alone.
# Round 13 — Reaction Lanes replaces Strike Rank. 9 Aug 2026
*Owner rejected Strike Rank as "too complex for Foundry"; then added that the game
must also run at a physical table with paper sheets, and that the random element
must be no more than 30-35% of the outcome.*
`R-117` **Strike Rank removed entirely and replaced by Reaction Lanes.**
Reflex = ceil(DEX / 3)
Awareness = ceil(readiness / 10)
Reaction Base = 2 x max(Reflex, Awareness)
Initiative = Current Base + d4 HIGHEST acts first
Quick thought fully substitutes for quick hands — `max()` rather than a sum is what
removes the combat-training bias. Verified live: Ceyla reaches base 12 through
awareness, Dunn reaches the same 12 through reflexes, Tomas through awareness,
Rakhi 10.
**The randomness ceiling drove the die size.** Measured as
`die range / (die range + printed-base range)` over a realistic envelope
(DEX 9-18, readiness 30-60):
| | share |
|---|---:|
| `3 x Lane + d6` as first proposed | 35.7% — over |
| Dual-Axis `Reflex + Awareness + d6` | 45.5% — well over |
| **`2 x Lane + d4`** | **33.3%** |
A d6 could never have worked: its range of 5 is too close to the entire printed
spread. Probabilities that follow: trained-and-advantaged versus
untrained-and-dragged is **100%** (the gap is 6, the largest opposed d4 swing is 3),
one band apart is **87.5%**, same band is a genuine **50%**.
`R-118` **The paper sheet drove the design.** The base is calculated once at
character creation and never again. Beneath it sits a seven-box **modifier rail**
(−3 to +3) filled in once; a clip marks the box in force. Aim, Edge, Drag and Load
move the clip — they never trigger a recalculation. At the table a round is: move
the clip if anything changed, roll one d4, add it to the marked number. **One die,
one addition, no lookup.** The digital sheet renders the same rail.
`R-119` **Coherence Flux without breaching the ceiling.** An additive ±1/±2 Flux
would have pushed the random share to 45-54%, so Flux **consumes the die instead of
adding to it**: at Coherence 3-5 the outer faces misfile (1 reads as 4, 4 as 1); at
0-2 every face inverts. The distribution stays uniform 1-4, so the ceiling and the
probability tables are untouched — but the die visibly lies, and a misfiled agent
acts at the wrong moment. Verified: Rakhi at Coherence 1 rolled a 3 and it was read
as a 2.
**Four self-inflicted breakages during the excision**, all caught by reading the
console rather than assuming: removing the Strike Rank block also deleted the
module-level `ceil` helper, `readinessRating()`, and `percentileFaces()`, each of
which other subsystems depended on; and the `game.ringbrp` API object silently
failed to attach because it referenced one of them. A single undefined name in an
object literal takes the whole init hook down with no error attributable to the
system. Restored, and the API block is now verified by script for undefined
identifiers.
# Round 13b — the invisible die, and the d6. 9 Aug 2026
`R-120` **The initiative die was never shown.** `rollReaction()` evaluated a `Roll`
and read `.total`, then posted a plain `ChatMessage` with rendered HTML. Foundry
only hands dice to Dice So Nice when a message actually **carries** the Roll, so
nothing animated and the roll was invisible — the owner reasonably concluded there
was no random element at all.
Fixed by pooling every combatant's die into one `Roll` via `PoolTerm.fromRolls()`
and attaching it to the chat card with `rolls: [pool]`. Verified: the card now
reports `isRoll: true` with **4 dice objects**, and `showForRoll` fires **once** for
the whole group.
A first attempt awaited `game.dice3d.showForRoll()` per combatant inside
`rollReaction`. That worked but serialised one full animation per character and
made rolling for a party hang long enough to time out. Removed — the pooled roll on
the message animates them all together.
`R-121` **d4 → d6 at the owner's request.** Random share rises from **33.3% to
45.5%** of the printed-base spread. That is above the 30-35% ceiling he set
earlier, and he accepted the trade knowingly. Observed spread across the party went
from 12-17 to 14-17 with more movement between rounds.
`R-122` **OPEN — three of the four agents share Reaction Base 12, and it is not a
bug.** Readiness 51-60 all round to Awareness 6, and Ceyla 55, Dunn 55 and Tomas 58
are genuinely within three points of one another. The banding is faithfully
reporting that the characters are similar. Options measured:
| awareness divisor | distinct bases | spread | cost |
|---|---:|---:|---|
| /10 (current) | 2/4 | 2 | — |
| /8 | 3/4 | 4 | awareness begins to dominate DEX |
| /7 | 3/4 | 6 | Dunn's DEX 18 stops mattering |
No change made: separating them means making awareness beat reflexes more often,
which is a design decision for the owner rather than a defect to fix.
# Round 14 — pregens differentiated, all content as Items, combat playtested. 9 Aug 2026
`R-123` **The pregens now pull the two initiative axes apart.** Three of four shared
Reaction Base 12 because their readiness was 55/55/58 — the mechanic was faithfully
reporting that the characters were nearly identical. Rebuilt so each sits clearly
on one axis:
| | DEX | reflex | readiness | aware | base | led by |
|---|---:|---:|---:|---:|---:|---|
| Ceyla | 10 | 4 | 71 | 8 | **16** | awareness |
| Dunn | 18 | 6 | 34 | 4 | **12** | reflex |
| Tomas | 14 | 5 | 58 | 6 | **12** | awareness |
| Rakhi | 11 | 4 | 47 | 5 | **10** | awareness |
Three distinct bases, spread 6. Ceyla is now the slowest on her feet and reliably
the first to act, which is the design working: she has usually worked out what is
happening a beat before it happens. Dunn and Tomas tie at 12 by *different routes* —
reflexes and awareness — which is the point of `max()` rather than a sum. Bios were
rewritten to match the numbers.
`R-124` **All content installed as Items.** `installContent()` now covers every pack
— skills, talents, weapons, armour, gear and devices — **125 items** browsable and
draggable in the Items directory, not just the 70 skills and talents.
## Combat playtest — three defects found and fixed
`R-125` **HP was calibrated for weapons that no longer exist.** `hitPointsFor` was
`CON + SIZ`, double standard BRP, set deliberately when the weapon ladder was the
1984 one (laser rifle 1d10+30). The setting rewrite replaced those with modern
agency issue averaging 3.5-9 damage, and HP was never brought down. Measured
result: **ten rounds of melee with nobody dropped and armour absorbing 51% of all
damage**. Now `(CON + SIZ) / 2` — Dunn 23 → 12, Rakhi 29 → 15.
Note for the owner: the Major Wound threshold is unchanged at 9, because the floor
was already doing all the work at both scales. But 9 is now 60-75% of a character's
pool rather than 35%, so a Major Wound is a much bigger event than it was. That
follows from your "floors at 9" ruling meeting the new HP scale — worth a look.
`R-126` **Human NPCs had natural armour.** Cleaners carried `naturalArmour: 2` on
top of a stab vest, an alien-hide leftover from the old setting, making ordinary
contractors better protected (4 AP) than the agents shooting at them (2-3 AP). Set
to 0 for all human NPCs; the Revenant keeps its 4 because it is not a person.
`R-127` **The compact pistol could not hurt anything.** 1d6 against 4 AP did
**nothing 66% of the time**, average 0.51 through — and it is the sidearm issued to
the Physician and Liaison roles, so those characters simply could not take part in
a firefight. Raised to 1d6+2, still the weakest firearm but functional.
Also: **Dunn was issued a baton but trained in knife**, so he was swinging at his
untrained 5% club skill. Reissued a utility knife.
## Result, 10 runs of each
| | before | after |
|---|---|---|
| Ranged, 2 PCs vs 2 Cleaners | **PC 0/10**, avg 8.4 rounds | **PC 7/10**, avg 8 rounds |
| Melee, 2 PCs vs 2 Keepers | PC 10/10, avg 6.5 | PC 10/10, avg 6 |
| Ranged, full team vs 3 Cleaners | not run | **PC 10/10**, avg 4.4 |
Armour now absorbs 28-39% rather than 40-51%. A fair fight is genuinely in doubt;
a full team against a smaller force wins in about four rounds. Melee remains
strongly PC-favoured, which is Rakhi: an entry bar with a +1d6 damage modifier
against unarmoured scavengers is meant to be decisive.
# Round 14b — Major Wound at half hit points. 9 Aug 2026
`R-128` **Major Wound is now `ceil(HP / 2)`**, replacing `max(9, HP/3)`.
The floor of 9 had been doing all the work at both the old and new HP scales, so
once HP halved it became 60-75% of a character's whole pool and a Major Wound
almost never fired. Half HP restores it to what it is meant to be: a blow that
takes you out of the fight, not one that nearly kills you outright.
Dunn HP 12 → MW 6 Rakhi HP 15 → MW 8
Tomas HP 13 → MW 7 Ceyla HP 13 → MW 7
A duplicate copy of the formula in `tools/content.mjs` still carried the old
`max(9, hp/3)` and was silently disagreeing with the engine — the packs were built
with one rule and played with another. Aligned.
Dunn's biography also still explained the retired floor ("his threshold would be 8
by the arithmetic; the floor raises it to 9") and carried a mangled phrase left by
an earlier blanket rename. Rewritten.
## Measured effect, 10 runs each
| | result | rounds | majors |
|---|---|---:|---|
| Ranged 2 v 2 | PC 8/10 | 6.9 | 23% of hits |
| Melee 2 v 2 | PC 10/10 | 4.9 | 19% of hits |
| Ranged 4 v 3 | PC 10/10 | 4.2 | 50% of hits |
Roughly one hit in five now causes a Major Wound in an even fight, and the ranged
2 v 2 improved from 7/10 to 8/10 with a round shaved off. The 50% figure in the
4 v 3 is the team's heavy weapons — Rakhi's marksman rifle and Ceyla's Error lance
— against 2 AP contractors, which is what those weapons are for.
# Round 14c — wound effects exercised and made real. 9 Aug 2026
*The hit-location path had never been run inside a fight; every previous playtest
used `resolveHit`, which skips locations entirely.*
`R-129` **Location sizes had not been rescaled with HP.** Fractions were set
against the old 26-point pool, so halving HP left a leg at 4 hit points against a
sidearm doing 4-6 through armour. Raised and, more importantly, set **explicitly by
location kind** rather than by chained string replacement — an earlier attempt at
this corrupted the values into `0.280` and `0.425` because each replacement ran over
the output of the last.
leg .42 · body .42 · vital .50 · arm .35 · head .40 · unit .28
Measured over 150 clean hits each, against armour 3:
| weapon | disables the limb | major wound |
|---|---:|---:|
| Compact pistol | 5% | 0% |
| Service pistol | 29% | 23% |
| Marksman rifle | 46% | 43% |
| Entry bar +1d6 | 56% | 53% |
A sidearm rarely cripples, a rifle often does, a heavy melee weapon usually does.
`R-130` **My own measurement was wrong first, and it was the R-93 trap again.**
The initial reading was "every hit disables a location, 6 of 6" and then "97-100%".
Both were artefacts: my harness reset with `{"system.locationDamage": {}}`, and
**Foundry merges object updates**, so nothing was ever cleared. Accumulated `taken`
values reached 382 and 434, which of course disabled everything. The fix is the
`-=` deletion form, exactly as recorded in `R-93` — and I did not apply my own
earlier lesson. The system was closer to correct than my test said.
`R-131` **Wound effects were printed and never applied.** The chat cards were
right — "The arm is useless. Two-handed actions are impossible; −30% Manipulation",
"A vital hit. Knocked down, and a Stamina roll or unconscious" — but nothing read
`disabledLocations`, and a *destroyed* leg left MOV at 10. Now derived in
`prepareDerivedData` and applied at the `skillRoll` choke point alongside
encumbrance and Coherence:
| state | MOV | effect |
|---|---:|---|
| healthy | 10 | shot at 59% |
| arm destroyed | 10 | shot at **29%** (−30 Manipulation) |
| leg disabled | **5** | −30 Physical |
| leg destroyed | **1** | prone |
Clean six-round firefight afterwards: 9 hits, 4 crippled limbs, 3 major wounds,
two shots stopped dead by armour, PCs won with Dunn at 7/12 and a battered leg.
# Round 14d — combat with wound effects live. 9 Aug 2026
The penalty is visibly load-bearing. From a narrated firefight, round 5:
FOE 45%-30= 15% roll 6 success
— a Cleaner whose arm had been crippled two rounds earlier, now shooting at 15%
instead of 45%. Dunn finished the fight on 1 hit point with MOV 5 and a ruined leg.
Across 12 runs of each engagement:
| | result | rounds | majors |
|---|---|---:|---:|
| Ranged 2 v 2 | PC 8/12 | 6.4 | 36 |
| Melee 2 v 2 | PC 12/12 | 5.5 | 22 |
| Ranged 4 v 3 | PC 12/12 | 4.4 | 57 |
**No death spiral.** Only 8-14 shots per engagement were fired by an already-wounded
combatant, because fights resolve before the penalty compounds. The wound matters
in the moment it lands without dragging the loser through a hopeless tail.
`R-132` **`destroyed` was tested against a single blow while `disabled` was
cumulative.** `resolveLocationHit` used `d >= max * 2` — this hit's damage — so a
limb could never be destroyed by repeated wounds, only by one enormous strike. The
actor's own derived data meanwhile computed `destroyed` from *cumulative* damage, so
the resolver and the sheet disagreed about the same limb: a location carrying 100
points of damage against a maximum of 6 reported `destroyed: false` from the
resolver and `destroyed: true` on the sheet. Now cumulative in both places.
That is the fifth duplicated-rule defect in this project (`R-97`, `R-100`, `R-104`,
`R-128`, and now this) — a rule implemented twice and changed in one place.
**Caveat on an earlier figure.** The "47% of hits crippled" in the trials counts
repeat hits on a limb that was *already* crippled, because `disabled` correctly
stays true once crossed. The honest per-hit rate is the controlled measurement:
5% for a sidearm, 29% service pistol, 46% marksman rifle, 56% entry bar.
# Round 15 — the duplicated-rule class, fixed at the root. 9 Aug 2026
Five separate defects in this project came from one shape: a rule implemented in
two places and changed in one — `R-97` the medical kit's bonus, `R-100` the phase
lead, `R-104` kit versus training bonuses, `R-128` Major Wound, `R-132` limb
destruction. Fixing them individually was not working, so this round fixes the
cause.
`R-133` **`tools/content.mjs` carried its own copies of the engine's rules**, under
a comment that said so out loud: *"rule functions mirrored from ringbrp.mjs"*. They
had drifted twice. Major Wound was `max(9, hp/3)` in the builder while the engine
used `hp/2`; and — found live during this audit — **hit points were still
`CON + SIZ` in the builder after the engine had halved them**, so the shipped packs
carried Ceyla at 26 HP and Rakhi at 29 while the game computed 13 and 15. Derived
data masked it on load, which is why playtesting never caught it.
**Fixed structurally.** New `rules.mjs` is the single authority for every pure rule
formula — hit points, Major Wound, damage modifier, marksmanship, location size,
the two Reaction axes and the base, encumbrance. It imports nothing from the
runtime and touches no Foundry globals, so both sides can use it: `ringbrp.mjs`
imports it in the browser, `tools/content.mjs` imports it in Node. Every local copy
deleted; verified zero re-definitions remain in either file.
`R-134` **A guard, so the class cannot come back.** `tools/check-rules.mjs`:
1. fails if anything outside `rules.mjs` re-defines a canonical rule,
2. asserts the builder's aliases *are* the canonical functions rather than copies
with the same name,
3. spot-checks eight formulas against known values.
`build-packs.mjs` now runs it first and **refuses to build** on failure. Proven by
re-introducing the exact `hitPoints = con + siz` regression: the check failed with
`content.hitPoints is not the same function as rules.hitPointsFor` and exit code 1.
Verified after rebuild: packs store 13/15/12/13 HP with Major Wounds 7/8/6/7, and
sheet and engine agree on every pregen for the first time.
`R-135` **Codex found the one the guard missed: constants are rules too.** The first
version of `check-rules.mjs` policed rule *functions* only. `ringbrp.mjs` still held
`RINGBRP.reaction = { dexDivisor: 3, readinessDivisor: 10, laneMultiplier: 2, die: 6,
modCap: 3 }` — a second literal copy of `rules.REACTION`, with its own copy of the
divisors — and two places recomputed `laneMultiplier * max(reflex, aware)` inline
rather than calling `reactionBaseFrom()`. Exactly the defect being fixed, surviving
inside the fix. Now `RINGBRP.reaction = REACTION` by assignment, both call sites go
through the canonical function, and the guard fails on any object literal carrying
two or more keys of a canonical constant set.
`R-136` **Discovery made the guard weaker, and the regression test caught it.**
Deriving the alias list by scanning for `export const X = Y;` meant that replacing an
alias with the drifted arrow function `(con, siz) => con + siz` stopped matching the
discovery regex — so it was never checked and the guard reported OK. The original bug
passed. Fixed by making a hand-held `REQUIRED` map authoritative, with discovery only
able to ADD: a missing alias now fails as loudly as a wrong one.
`R-137` **The same drift in prose.** The Reaction comment still described a d4 and
quoted d4 probabilities after the die changed to d6; `README.md` still advertised
`CON + MAS`, `max(9, ceil(HP/3))` and gravity-based marksmanship. Both corrected, and
the code comment no longer restates any formula — it points at `rules.mjs` and carries
only measured behaviour.
**Guard now fails on four escape routes, each verified by deliberate regression:**
an alias swapped for a drifted copy; an alias deleted; a rule defined in a brand-new
file; a rule added to `rules.mjs` with no spot-check. Plus the constant-literal case.
**Measured, and worth the owner knowing:** on the shipped pregens the d6 puts the
random share at **45.5%**, above the 30–35% ceiling that was set. One band apart wins
72.2% (81.3% excluding ties). The d6 was chosen deliberately for table feel over the
d4 that hit 33.3%. `REACTION.laneMultiplier: 2 -> 3` returns the share to 35.7%
without touching the die — a one-line change, left undone because it was not asked for.
`R-138` **laneMultiplier 2 -> 3, at the owner's instruction.** The random share is
now **35.7%**, inside the 30–35% ceiling. Verified live: bases 24 / 18 / 18 / 15.
Measured on the shipped party over 200,000 rounds, both settings, load applied:
| | share | Ceyla | Dunn | Tomas | Rakhi |
|---|---:|---:|---:|---:|---:|
| laneMultiplier 2 | 45.5% | 73.1% | 14.8% | 12.1% | 0.0% |
| **laneMultiplier 3** | **35.7%** | **94.8%** | **2.8%** | **2.4%** | **0.0%** |
The ceiling was bought with order. Cutting the die's share does not spread the lead
around — it concentrates it. Ceyla goes from a strong favourite to a near-certainty,
and the two mid agents lose most of their share of the lead. Same band is still a
50/50 (16.7% ties, broken by DEX) and one band apart is 83.3%, but **two bands apart
is now 100%**: a six-point gap cannot be closed by a d6 at all.
Rakhi never leads at either setting, so that is not caused by this change. It is her
−2 Load from carrying 38 kg against a free carry of 18. Dropping roughly 20 kg returns
her to 15 and puts her level with Dunn and Tomas — a real decision on the sheet rather
than a fixed disadvantage, which is the intent.
`R-139` **A third duplicate, found while measuring.** `prepareDerivedData` recomputed
encumbrance inline — its own `Math.floor(over / 10)`, `steps * 5` and `over / 10` —
instead of calling `encumbranceFrom()`. It also wrote the over-weight under the key
`overKg` while `rules.mjs` returned `over`, so simply routing the call through the
canonical function would have silently zeroed every encumbrance penalty, because
`loadBand()` reads `overKg`. Fixed by returning both names from one computation and
saying in the source why both exist. The inline block is gone.
**Still open.** `srLoad` — the exact fractional cost per kilogramme, written for the
old Strike Rank so no weight was ever free — is computed and displayed but no longer
used: Reaction Lanes reads `loadBand()`, a three-value integer band (0 / −1 / −2).
The granular rule the owner asked for survives in the data and not in play.
`R-140` **srLoad wired into the rail — the granular rule is in play at last.** The
exact per-kilogramme cost, computed since R-23 and consumed by nothing, now reaches
initiative. The three-value band (`0 / −1 / −2`) is gone.
**Load is not a rail position.** It is a property of what the agent is carrying,
known before the fight starts, so it is subtracted into the number printed on the
sheet rather than occupying a marker slot. The rail is now situation only — Aim,
Edge, Drag — which is what it should always have meant, and load can no longer be
double-counted by moving the clip.
| | old band | now |
|---|---:|---:|
| 0.1 kg over | −1 | −0.1 |
| 1 kg | −1 | −0.1 |
| 5 kg | −1 | −0.5 |
| 10 kg | −1 | −1 |
| 20 kg | −2 | −2 |
| 30 kg | −2 | −3 |
Gentler where it was absurd — the old band charged a full point for the first gram
over and then nothing more for the next 10 kg — and heavier where it was too kind,
since the cap at −2 meant nothing above 20 kg cost anything at all.
**Rounded up, not to nearest.** To nearest made the first half-kilogramme free, which
verified live: a 1 kg weight added to Dunn moved his printed value not at all. `ceil`
to one decimal makes 100 g over cost 0.1 and nothing cost nothing, which is the rule
as stated. One decimal is the smallest unit worth writing on a paper sheet.
**Floored at 1** — R-56 applied to encumbrance. No amount of kit reduces an agent to
acting never; it reduces them to acting last. A 300 kg overload prints 1, not −15.
`R-141` **Ordering bug caught before it shipped.** `prepareDerivedData` derived the
reaction block *before* encumbrance, so the printed value read an unset
`sys.encumbrance` and would have silently printed a load of zero for every actor —
the same silent-wrong class as the double hit points. The block is moved below
encumbrance with the dependency stated in the source.
Verified live: Ceyla 15.4 kg against a free carry of 12 prints **23.6** (base 24 less
0.4); Rakhi at 38 against 18 prints **13**. Adding 1 kg to Dunn moves him 18 → 17.9
and removing it restores 18. Turn order sorted correctly on decimals, and
`CONFIG.Combat.initiative.decimals` was already 1.
# Round 16 — combat played under the new load rule. 9 Aug 2026
Two fights, four agents against four Cleaners, opening at 25 m and closing to
contact. Driven through the real engine — `resolveBands`, `gradeRoll`,
`rangeBandFor`, `armourAgainst`, `damageAfterArmour`, `hitLocationRoll`,
`majorWoundRoll`, `woundPenaltyFor` — not a hand simulation.
**Fight one: total party kill, three Cleaners still standing.** Tomas and Ceyla
both dropped to single critical hits, which ignore armour entirely. Rakhi survived
six rounds of concentrated pistol fire behind a breaching suit and then lost to a
critical. Losing two agents in the first two rounds against equal numbers is not
recoverable.
**Fight two: agents win in five rounds, three standing, all four Cleaners down.**
The difference was Rakhi dropping her pack.
`R-142` **The load rule works, and it produced a real decision.** Rakhi begins at
38 kg against a free carry of 18 — Load −2, printing 13, the slowest lane in the
team. In round one she acted **last of eight**, behind all four Cleaners. She spent
round two dropping her pack: 17.4 kg, Load 0, printing 15. From round three she
acted third or better every round and killed a Cleaner with the prybar.
Measured over 100,000 rounds against a Cleaner printing 12:
| Rakhi | acts first | tie |
|---|---:|---:|
| carrying 38 kg (prints 13) | 58.6% | 13.9% |
| pack dropped (prints 15) | 83.2% | 8.3% |
A 24.6 point swing for one action. That is the trade the rule exists to create, and
it is legible at the table: the number on the sheet changes when the pack comes off.
`R-143` **Armour is the dominant variable in Major Wounds, far more than hit points.**
Frequency of Major Wounds among service-pistol hits that beat armour, 200,000 trials:
| | armour | MW threshold | hits through | of those, Major Wounds |
|---|---:|---:|---:|---:|
| Ceyla | 6 | 7 | 50% | **0.0%** |
| Rakhi | 7 | 8 | 38% | **0.0%** |
| Dunn | 3 | 6 | 88% | **28.6%** |
| Tomas | 2 | 7 | 100% | **24.9%** |
A pistol maxes at 10, so 6+ armour makes a Major Wound arithmetically impossible on
a normal success, while 2–3 armour takes one every fourth hit. The pressure valve is
the critical, which ignores armour — and in both fights every agent who went down
went down to a critical. That is a defensible design, but the cliff between armour 3
and armour 6 is much sharper than the two-point difference on the sheet suggests.
`R-144` **Major Wounds change no roll.** The most serious finding. `majorWoundRoll`
rolls 1d10, resolves the table, posts a chat card with the full effect text, records
the entry in `system.wounds`, and shows it on the sheet — and stops there.
`woundPenaltyFor` reads `system.wounded`, which is derived **only** from
`locationDamage`, so:
- a `manipulation` Major Wound, whose own text reads "−30% Manipulation", applies 0
- a `mobility` Major Wound, "MOV halved", leaves MOV at 10
- `concussed`, "−20% to all rolls", applies 0
Verified directly: with `manipulation` and `mobility` both recorded in
`system.wounds`, every `woundPenaltyFor` category returns 0 and MOV is unchanged.
Only `crippling` and `catastrophic` do anything at all, via the `isDown` check.
Disabled *locations* do work correctly — a destroyed leg gives `physical −30`,
`movFactor 0.1`, `prone true`, and a destroyed arm gives `manipulation −30`. It is
the Major Wound table specifically that is prose. Same class as `R-102`.
Observed live in fight two: Ceyla took a manipulation Major Wound in round two and
shot at exactly the same 53% in round three.
**Not a bug found this round:** I first reported `woundPenaltyFor` as broken because
it returned 0 for `"physical"` and `"manipulation"`. Those are `system.wounded`
field names, not category ids. The ids are `phys`, `manip`, `melee`, `ranged`,
`comm`, `mental`, `percep`, and the function handles all seven correctly.
`R-145` **Major Wound effects wired into the penalty pipeline — R-144 closed.** The
table is no longer prose. `woundEffectsFor(wounds)` in `rules.mjs` turns each row
into numbers, `prepareDerivedData` folds them into `system.wounded` on top of the
location-derived state, and `woundPenaltyFor` reads the result at the existing
`skillRoll` choke point. No new choke point was added: everything that already
respected a shattered arm now respects a concussion.
| Wound | Applied |
|---|---|
| concussed | −20% to **every** category, via a new `all` term |
| winded | −20% Physical, `lostAction` flag |
| bleeding / deepBleeding | 1 and 2 hp per round, exposed as `wounded.bleed` |
| manipulation | −30% manipulation, melee and ranged |
| mobility | −30% Physical, MOV halved |
| sensory (sight) | −40% Perception, −40% manipulation/melee/ranged |
| sensory (hearing) | −40% Perception, −40% Communication |
| broken | as manipulation or mobility, by variant |
| crippling | defence only, MOV 1, counts as down |
| catastrophic | unconscious, removed from the fight |
Three decisions worth recording:
**They stack, and they stack with locations.** Two concussions are −40%; a
concussion plus a destroyed leg is −50% Physical with MOV 1 and prone. Both facts
are true about the same agent, so the numbers add rather than one overwriting the
other. Verified live.
**The 1D2 variants are rolled and stored.** `sensory` and `broken` each split two
ways. That roll used to be left to the GM because nothing consumed it; now the
penalty depends on it, so `majorWoundRoll` rolls it, stores it on the wound entry
and prints it on the chat card. **A missing variant falls back to the harsher
reading**, so an old wound saved before this change can never quietly cost nothing.
**`isDown` now reads the derived flag** rather than re-testing the wound keys, which
was a second place that knew which wounds end a fight.
Verified through the real `skillRoll`, not a reimplementation:
```
healthy Firearm (Pistol) 59% → 59% ≤3 ≤12 ≤59 ≥98
concussed (−20 all) 59% -20% → 39% Wounded: -20% ≤2 ≤8 ≤39 ≥97
concussed + manipulation 59% -50% → 9% Wounded: -50% ≤1 ≤2 ≤9 ≥96
cleared 59% → 59% ≤3 ≤12 ≤59 ≥98
```
Critical, special and fumble bands all move with the penalty, and the chat card
names the cause. In a live fight Tomas took a catastrophic wound at 1 hp and was
correctly removed from the turn order for the rest of the encounter.
Guard extended to 13 rules and 35 formulas, including the missing-variant fallback.
**Two harness errors of mine, recorded so they are not re-found as engine bugs:**
`woundPenaltyFor` takes category ids (`phys`, `manip`), not `system.wounded` field
names; and `skillRoll` takes `rating` and `categoryId`, not a `skill` object —
passing one silently rolls against 0%.
# Round 17 — combat with wounds live, and a correction. 9 Aug 2026
`R-146` **Correction: every fight in rounds 15 and 16 ran at double damage.**
`hitLocationRoll` applies the hit points itself (`ringbrp.mjs:929`) as well as
recording the location. My harness subtracted the same damage a second time. The
engine was never wrong; the playtests were.
What this invalidates: the fight narratives in R-142 and R-144 — the total party
kill, "every agent who went down went down to a critical", and the impression that
Major Wounds rarely fire. What it does **not** touch: the load measurements in R-142
(initiative does not depend on damage) or the armour arithmetic in R-143 (computed
directly, not from a fight).
`R-147` **Wounds re-tested at correct damage, and they carry the fight.** Four agents
against two marksmen and two contractors at 30 m, twelve rounds. Eight Major Wounds
fired and every one of them changed what happened next:
| | |
|---|---|
| Dunn | **catastrophic**, round 1 — unconscious, removed from the order, never acted |
| Tomas | **manipulation** — pistol 43% → 13%, missed everything after and died at 3 hp |
| Marksman | **manipulation** — rifle 55% → 25% |
| Marksman | **broken (manipulation)** — 55% → 25%, MOV 5 |
| Rakhi | **deepBleeding** — 2/round from 10 hp, dead in round 6 |
| Marksman, Cleaner, Cleaner | **bleeding / deepBleeding** — all three bled out |
**Bleeding decides fights.** Four of the six casualties died of blood loss rather
than the blow that caused it, including Rakhi, who was on 10 hit points when she took
it. Two per round against 12–15 hit points is roughly a five-round death sentence and
there is currently no First Aid step in the loop to stop it — the rule assumes
someone spends an action, and nothing in play forces that choice to be visible. This
is the wound effect with the largest influence on outcomes and it deserves a check.
**The -30% wounds are decisive rather than flavour.** Tomas at 13% and the marksmen
at 25% were all still standing and effectively disarmed. That is the intent of the
table text, now actually true.
`R-148` **Stalemate at the tail.** Rounds 9 to 11 produced nothing: Ceyla at 53%
against a marksman at 55%, armour 6 against 1d10+2, both missing or being stopped.
Two roughly equal combatants with good armour and mid skills can circle indefinitely.
Worth a pressure mechanic — a closing clock, or escalating penalties for a stalled
exchange — rather than leaving it to the dice.
Wound keys not seen this fight — `concussed`, `winded`, `mobility`, `sensory`,
`crippling` — were verified separately against every category and MOV in round 16.
`R-149` **Bleeding fixed — the rule was missing both halves, failing in opposite
directions.** The table printed a rate and a cure and the game implemented neither:
- **Nothing ticked the loss.** `wounded.bleed` was derived and displayed, and no
code ever subtracted it. At an actual Foundry table bleeding did *nothing*.
- **Nothing stopped it.** `firstAid` healed hit points and never cleared the wound,
so once my harness applied the loss it ran until the target was dead.
That is why R-147 read as a death sentence: my simulation supplied the half the
engine was missing and none of the half that ends it.
**Both halves now implemented.** `BLEED` in `rules.mjs` is the single authority for
the rate and for what stops it; `woundEffectsFor` reads it rather than carrying its
own copies of 1 and 2.
| | per round | stopped by |
|---|---:|---|
| bleeding | 1 | First Aid, or Medicine |
| deepBleeding | 2 | **Difficult** First Aid, or any successful Medicine |
`applyBleeding()` runs from the `updateCombat` hook; `stopBleeding()` is called by
`firstAid` on success and by the new `medicalCare()`, both routed through
`bleedStoppedBy` so there is one authority on what qualifies.
**One deviation from the printed text, deliberately.** The table stops plain
bleeding with First Aid only. Medicine now also stops it, because the rule as
written left a surgeon unable to close the lesser wound they could treat.
**A free tick, caught in testing.** The hook fired on entering round 1, charging a
bleeding agent before any round had elapsed — 15 hp became 11 by round 2 instead of
13. Now only round 2 and later charge.
**No rebalancing was needed, and I checked before assuming.** Survival with a medic
retrying each round, 50,000 trials:
| from | Difficult First Aid (23%) | Medicine-grade (46%) |
|---:|---:|---:|
| 15 hp | 87.6% | 99.3% |
| 12 hp | 78.8% | 97.5% |
| 10 hp | 72.9% | 95.5% |
| 5 hp | 54.5% | 84.3% |
Bleeding was never too harsh — it was unstoppable. Treatable, it costs the medic an
action every round and still usually works, which is the decision the rule wants.
A single unlucky live run took seven attempts; the distribution says four.
Verified live: the clock runs on round change, an Average First Aid does **not**
close a deep bleed, a Difficult one does, Athletics does not, plain bleeding closes
on an Average First Aid, and the loss stops at 0 rather than grinding negative.
Guard now covers 15 rules, 2 constant sets and 44 formulas.
# Round 18 — bleeding in play, and a defect that outranks it. 9 Aug 2026
Fights run on a real `Combat` document this time, so initiative came from
`rollReaction` and blood loss from the engine's own `updateCombat` hook rather than
anything in the harness.
`R-150` **Bleeding works in play.** Two bleeds fired in the reported fight and both
became attrition clocks rather than announcements:
```
R3 Tomas pistol 43% → Marksman 2 critical — 8 dmg (hp 6)
** BLEEDING ** bleeds 1/round
[engine bleed] Marksman -1 → 5
R4 [engine bleed] Marksman -1 → 3 ← dead to a critical the next round
R5 Dunn knife 34% → Cleaner 2 critical — 6 dmg (hp 8)
** DEEPBLEEDING ** bleeds 2/round
[engine bleed] Cleaner -2 → 6
R6 [engine bleed] Cleaner -2 → 4
```
Both wounded hostiles died at least partly to the bleed. The tick fires once per
round, from the engine, with no harness involvement. The First Aid branch did not
trigger only because both bleeds landed on hostiles; staunching was verified
directly in R-149.
Wound penalties were visible throughout: Tomas's pistol fell 43% → 23% and Rakhi's
rifle 47% → 27% from concussions, for the whole rest of the fight.
`R-151` **Initiative is rolled once and frozen for the entire combat.** The larger
find. Foundry keeps `combatant.initiative` across rounds and nothing re-rolls it, so
the d6 is drawn once per fight, not once per round. Verified over four rounds — the
same four numbers, unchanged:
```
round 1: Ceyla 25.6 · Dunn 22 · Tomas 19 · Rakhi 15
round 2: Ceyla 25.6 · Dunn 22 · Tomas 19 · Rakhi 15
round 3: Ceyla 25.6 · Dunn 22 · Tomas 19 · Rakhi 15
round 4: Ceyla 25.6 · Dunn 22 · Tomas 19 · Rakhi 15
```
Three reasons this is wrong rather than merely a choice:
1. **The Aim rail is inert.** "Aim one round, fire the next" for +1/+2 cannot do
anything if the order never recomputes. The entire seven-box rail — Aim, Edge,
Drag — only means something between rounds. It currently means nothing after
round 1.
2. **The measured random share is per round.** 35.7% was tuned as round-to-round
variance. Drawn once, the variance is 0% for the rest of the fight and one draw
decides the whole engagement.
3. **The engine's own description says otherwise** — "current printed base + one
die" per round, and load, wounds and coherence all change mid-fight without ever
being re-read.
Seen in the fight above: the order was identical for all seven rounds, and the
marksman that Ceyla out-ran in round 1 stayed out-run until it died.
Not yet fixed — it changes every fight and is the owner's call.
`R-152` **Not a bug: the Major Wound d10 is uniform.** Three catastrophic results in
one fight looked rigged. 4,000 draws give every row 9.3–11.0%. Variance, not a
weighted table.
`R-153` **Initiative re-rolls every round — R-151 closed.** `RingBRPCombat.nextRound()`
now resets and re-rolls after `super.nextRound()`. Six consecutive rounds gave six
distinct orders; before the change, four rounds gave the same four numbers.
```
r1: Ceyla 28.6 Dunn 21 Tomas 20 Rakhi 16
r2: Ceyla 25.6 Dunn 22 Tomas 19 Rakhi 19
r3: Ceyla 25.6 Dunn 23 Tomas 21 Rakhi 16
r4: Ceyla 27.6 Tomas 24 Dunn 22 Rakhi 19
r5: Ceyla 27.6 Dunn 22 Tomas 19 Rakhi 18
r6: Ceyla 28.6 Tomas 24 Dunn 21 Rakhi 15
```
Three consequences deliberately handled rather than left to fall out:
**Aim is spent, not permanent.** Aim is read by the roll it paid for and then
cleared, so "aim one round, fire the next" costs a round and buys one round. Left
alone it would have granted +2 for the rest of the fight the moment re-rolling made
it matter. Verified: mod 2 applied on the next round, flag 0 after, mod 0 the round
following. Edge and Drag persist — they describe equipment and conditions, which do
not evaporate at a round boundary.
**The downed are not re-rolled.** Only combatants above 0 hit points and not
unconscious draw a die; the rest keep a null initiative, which the existing
comparator already sorts to the bottom. Before this, a corpse drew a d6 every round
and could sort above the living. Verified: Rakhi bleeds to 0 in round 3 and holds
"—" from then on.
**The re-roll runs after blood loss**, so an agent who bleeds out in the round
transition does not receive a fresh initiative first.
**Mid-fight changes now reach the order at last.** Rakhi dropping her pack moved her
printed value 13 → 15 and her very next roll used 15. Under frozen initiative that
decision — the one the whole load rule exists to create — could not affect the fight
it was made in.
Played through: order churned every round (the marksman ran 24 → 22 → 24 → 19,
Dunn and Tomas swapping repeatedly), while Ceyla still led all five rounds. That is
`R-138` visible in play, not a fault in the re-roll: at laneMultiplier 3 she leads
94.8% of rounds. Re-rolling changes the *middle* of the order every round and leaves
the top almost fixed.
Also seen: Dunn's manipulation Major Wound took his knife from 34% to **4%**, and it
stayed there for the rest of the fight.
# Round 19 — Strike Rank residue, and the missing damage roll. 9 Aug 2026
`R-154` **Strike Rank survived in three talents and four dead lang keys.** The
mechanic was removed in round 12; its vocabulary was not. Found by sweeping code,
templates, language, packs and the live world:
| | was | now |
|---|---|---|
| First Through | "declare an action at Strike Rank 1 once per fight" | "act first in the round no matter what anyone rolled" |
| Out of Step | "act as though your Strike Rank were 1 lower" | "+3 Reaction for that round — a full lane" |
| Quorum | "one additional non-combat action, at Strike Rank +2" | "resolved 2 points lower on the Reaction order" |
**Four language keys were referenced by live code and did not exist.**
`RINGBRP.SR.ReadinessSkill` and `RINGBRP.SR.ReadinessInt` are read every time a
character sheet renders, so the sheet was printing the raw key string. Renamed to
`RINGBRP.React.*` and supplied ("from training" / "from wits").
`RINGBRP.SR.DeclareHint` and `RINGBRP.SR.Action` belonged to
`templates/dialog-declare.hbs`, the old declare-your-action dialog, which nothing
has opened since Strike Rank was removed. Template deleted.
Two mentions remain deliberately: the comment above `rollReaction` and a README
row, both of which say Reaction Lanes *replaced* Strike Rank. Those are history,
not rules.
**A note on the search.** My first pack scan reported zero hits because
`ClassicLevel` could not open the databases while Foundry held the lock, and the
`try/catch` swallowed it — the same shape as `R-98`, a check that reports success
when it has not run. Re-run against the live world, it found eight.
`R-155` **A successful attack did not offer its damage.** The roll ended at
"Success" and left the player to find the weapon on the sheet and click it
separately — and that path did not know what had been rolled, so **a critical did
ordinary damage**.
Fixed at the cause, which was again duplication: `_onRollDamage` existed **twice**,
once on the character sheet and once on the item sheet, near-identical and neither
aware of the success level. Both now delegate to one `rollWeaponDamage({ actor,
weapon, level, band })`, and successful attack cards carry a **Roll damage** button
bound to it.
Success levels now reach the dice (spec §4.4):
```
critical → 6, 6, 6, 6, 6, 6 maximum weapon damage, armour ignored
success → 6, 4, 5, 3, 3, 5 normal 1d4+2, full armour
```
The damage modifier is still *rolled* on a critical rather than maximised, per the
rule. The card carries the note — "maximum damage, armour ignored" for a critical,
"armour counts half" for a special — so the level is not lost between the two rolls.
The button disables itself once used, so one hit buys one damage roll, and it is
bound on render rather than inline so it still works after a reload.
Verified live: button present on critical, special and success; absent on failure
and fumble; clicking produces "Utility knife — damage — armour counts half", total 6.
`R-156` **Kit dropped onto a character was not readied.** The catalogue packs ship
armour and weapons with `equipped: false` — correct for a compendium entry, wrong
the moment one lands on an actor. A vest dragged onto a sheet contributed **0
armour** and a pistol never reached the READIED strip. Nothing errored: the item was
there, the number simply did not move, which is the hardest kind of defect to notice.
Fixed with a `preCreateItem` hook. **Only the first of each kind is readied** —
a spare vest or a second pistol stays stowed, because silently stacking armour points
or crowding the strip is the opposite failure. Anything arriving explicitly equipped
(pregens, generated characters) is untouched.
| dropped | result |
|---|---|
| 1st armour | worn, armourPoints 6 |
| 2nd armour | stowed, armourPoints stays 6 |
| 1st weapon | readied |
| 2nd weapon | stowed |
`R-157` **"ARMOUR 0" now says why.** Carrying armour you have not put on displayed
as a bare zero beside a list containing a vest, which reads as a broken sheet rather
than a state of the fiction. The box now turns red and prints **NOT WORN**, with a
tooltip pointing at the shield toggle on the Gear tab. Verified rendering as
"ARMOUR 0 NOT WORN".
This was the reported symptom. The underlying cause was `R-156`; the display change
is so that if it ever happens again — a player unequips deliberately — the sheet
says so instead of looking wrong.
`R-158` **`generateCharacter` threw on an unknown role.** `ROLES[role]` came back
undefined and the next line read `R.stat`, so a typo in a macro produced a stack
trace instead of a character. It now falls back to a random valid role, the way
`tier` already did. Found while testing the armour paths, not by design.
Existing actors were checked and are unaffected: Ceyla 6, Dunn 3, Rakhi 7, Tomas 2.
# Round 20 — legibility. 9 Aug 2026
`R-159` **Installed items now file themselves into folders.** `installContent` put
125 items into one flat alphabetical column, where a Service pistol sits between
Science (Physics) and Sleight of Hand and nothing can be found. One folder per pack:
Skills 52 · Talents 18 · Weapons 18 · Armour 10 · Gear & Devices 27, unfiled 0.
Folders are matched by name on re-install so running it twice does not produce
"Weapons (2)", and items already installed flat are swept into place, so an existing
world tidies itself rather than keeping two conventions at once.
`R-160` **The Reaction number now shows its working.** "23.6" on its own is
unreadable — it does not say whether the agent is quick or merely alert, nor what
the kit is costing. A collapsible breakdown sits under the rail:
```
Reflex DEX 10 ÷ 3, rounded up 4
Awareness Readiness 71 ÷ 10, rounded up 8
Readiness comes from training: your best of Tradecraft,
Insight, Spot or Listen (71) — wits would only give 51
Lane the better of the two × 3 24
Load 15.4 / 12 kg — 3.4 kg over −0.4
Reaction on the sheet — then add d6 23.6
```
The Readiness line is the part that was genuinely invisible: it is the best of
**Tradecraft, Insight, Spot or Listen**, floored at **INT × 3** so a clever but
untrained agent is never flat. Ceyla is an analyst — DEX 10 gives her Reflex 4, but
Tradecraft 71 gives her Awareness 8, and the better of the two sets her lane. That
is the design working as intended: she is first in the order because she is alert,
not because she is fast.
Put in the reaction area rather than the character's notes, deliberately — notes are
free text and would go stale the moment a skill or a pack changed, which is the
duplication defect this project keeps re-learning.
`R-161` **"Error lance" renamed.** The name came from the setting premise — a
crossing is an induced filing error — but it reads as a bug rather than a weapon.
Renamed with its sibling, since "Error cutter" had the same problem and leaving one
would have broken the pair:
| was | now |
|---|---|
| Error lance | **Redaction lance** |
| Error cutter | **Excision cutter** |
Both keep the bureaucratic-horror register the setting asks for — the Custodians
correct the record — without naming a weapon after a fault condition. The journal
text was updated with them.
# Round 21 — the sheet as a thing you read. 9 Aug 2026
`R-162` **The species drop-down is gone from Anatomy.** Every character sheet
offered a choice of all three anatomies, which is not a decision anyone makes while
playing — a Baseline is not one option among three, it is what the character is. The
tab now names the species and shows only its locations. The selector moved to Bio,
where identity lives, with a note that changing it changes the anatomy.
`R-163` **A figure, drawn per species, on the Combat tab.** `RINGBRP.bodyArt` gives
each species profile a shape per location, keyed by the same ids as
`RINGBRP.locationTables` — so the drawing and the table are the same data and a
location cannot appear in one without the other. A species with no art simply draws
nothing rather than drawing a human by mistake.
The anatomy is honest rather than generic: **Baseline** is a seven-part humanoid;
**Vesh** is asymmetric with one long grasping limb, one short bracing limb, a leaning
trunk and a sensory ridge where a head is not; **Cadence** is five separate bodies,
equal and unranked, which is the point of them.
Five states, readable at sheet size without reading a number:
whole · grazed (any damage) · hurt (half) · disabled · destroyed (dashed outline).
Hovering a limb highlights its table row and the reverse, so the picture and the
numbers are visibly the same thing.
`R-164` **The location table was ragged because every column sized itself.** Now
`table-layout: fixed` with a flexible name column, fixed 3.6em numeric columns,
right alignment and `font-variant-numeric: tabular-nums`, so digits line up down the
column instead of drifting with content width.
`R-165` **Wounds moved onto the Combat tab**, beside the figure, with the full
effect text and the clear button. They remain on their own tab as well — nothing was
taken away, the information was simply put where it is used.
`R-166` **A wounded agent is now visible on the first tab.** Major Wounds modify
every roll on the Skills tab and were legible only two tabs away. A red bar sits
above the skill list naming each wound and the arithmetic it is applying:
```
WOUNDED [Concussed] [Bleeding] all rolls -20% · Manipulation -30% · bleeding 1/round
```
It only appears when there is something to say.
Verified live across all three species: 7 / 6 / 5 parts drawn, states resolving
correctly against real location damage, drop-down absent from Anatomy and present on
Bio, wound bar rendering with the right numbers.
# Round 22 — the rules go in the box. 9–10 Aug 2026
`R-167` **A rules journal, generated from the rules.** `packs/rules` ships
"The Custodians — Rules", seven pages, built by `tools/rules-text.mjs` from
`rules.mjs` and `lang/en.json`. **No number in the book is typed by hand.** Hit
points, Major Wound thresholds, the damage ladder, the reaction formulas, the wound
table and its consequences, coherence bands and costs, the bleed rates, the range
ladder and the encumbrance table are all computed at build time; every wound and band
name is read from the language file, so the book and the interface cannot disagree.
That defect has landed five times in this project — a hand-written rulebook would
have been the sixth.
Shaped to be learned and then abandoned:
| Page | |
|---|---|
| 1 · The whole game | the d100 core, the three modifiers, where the numbers come from |
| 2 · Fighting | reaction, the turn, range, what a hit does |
| 3 · Getting hurt | locations, the Major Wound table, bleeding |
| 4 · Coherence | the setting mechanic |
| 5 · A case | the session structure |
| 6 · Reference | tables you look up, never read |
| 7 · A round, worked through | Ceyla's real sheet numbers, start to finish |
Page 1 says explicitly that it is enough to play, and page 7 walks one round using
figures a reader can check against the shipped pregen.
`R-168` **Four more rule tables moved into `rules.mjs`** so the journal could be
generated from them at all: the circumstance ladder, the range ladder, the Major
Wound table and the coherence bands, costs and recovery. `ringbrp.mjs` now assigns
rather than re-declares them, `rangeBandFor` and `coherenceBand` are aliases of the
shared functions, and the guard covers all of it — 17 rules, 8 constant sets, 54
formulas.
`R-169` **The printed chance was not the real chance.** `resolveBands` returned a raw
`effective`, so a skill beaten below zero printed **0%** on the chat card. The floor
was genuinely there — `gradeRoll` tests the critical band first, so a 1 still
crits — but it arrived sideways, and the card contradicted the rule. `effective` is
now the clamped figure. Verified: rating 1 at Difficult prints 1% and succeeds 1% of
the time; every band from 1 to 100 now prints what it actually does.
`R-170` **The 96–00 rule was in the engine and in no document.** `gradeRoll` has
always failed 96–99 regardless of skill and fumbled on 00 — correct BRP, and the
reason a 100% skill really succeeds 95% of the time. It appears nowhere in the design
notes and would have appeared nowhere in the book. Now on page 1 and in the reference.
`R-171` **Correction: my combat harnesses double-counted armour.** `hitLocationRoll`
applies armour itself and takes an `armourApplied` flag for callers that have already
done it. My harnesses deducted armour and did not pass the flag, so every playtest
from round 16 onward understated damage. The engine was right throughout.
Re-run with armour counted once, the same fight resolves in **4 rounds instead of
7–12**, and Tomas goes down in round one. Combat is materially more decisive than my
earlier reports suggested — worth knowing before any further balance work.
**Sweeps run this round.** Generators across every role, tier and species (85 checks);
subsystems — wounds, bleeding, healing, coherence, skill rolls, encumbrance edges,
sheets, tables, every pack (25 checks); talents and progression (6 checks). Everything
that failed was my test's assumption rather than the system, except `R-169`, which was
real. `check-rules` is green and gates the build.
# Round 23 — the table, and a species that does not exist. 10 Aug 2026
`R-172` **The hit-location table was unreadable, and `table-layout: fixed` was why.**
The header read `MELEERANGETAKENARMO`, ranges wrapped onto two lines, rows were
different heights and the numbers sat a hand's width from the names they described.
Three separate causes, all mine:
1. **Fixed layout sizes every column from the first row** — the header row, where
the font is 0.62rem. `width: 3.6em` therefore resolved to about 39px, too narrow
for "RANGED", so the headers overflowed into each other and the ranges wrapped.
2. **`.rb-loc-cell` was `display: flex` on a `<td>`**, which takes the cell out of
table layout altogether — it had been there since before these columns existed.
3. **The name column was given `width: 99%`**, so on a wide sheet it absorbed 352px
of slack and threw the numbers to the far edge of the panel.
Now: auto layout, `white-space: nowrap`, content-sized columns, a fixed 22px gutter
after the name, and `width: auto` so the table is only as wide as it needs to be.
Measured in isolation at four container widths — 1400, 900, 640 and 420px — the
table is **297px every time**, every row **20px**, the last column right-aligned to
within a pixel, across Baseline, Vesh, Cadence and a wounded sheet.
Also: the roll button carried a whole sentence and wrapped to three lines — short
label, sentence moved to the tooltip — and "No active wounds." was centred in a wide
empty box instead of sitting under its heading.
**A note on verification.** Foundry reports its window as 0×0 in this browser, so
layout cannot be measured in place. The check was done by lifting the real markup
and the real stylesheet into a shadow root at controlled widths — which measures the
shipped CSS rather than a mock-up of it.
`R-173` **Every packed actor was a species the game does not define.** Ten of them —
all four pregens and all six NPCs — shipped with `speciesProfile: "hominid"`, a name
left from the Ringworld setting. It is not a key in `locationTables`, so the sheet
fell back to Baseline and nobody noticed: the hit locations were right by accident,
and would have been silently wrong the moment a Vesh or a Cadence was built from a
pack.
Found by accident while restoring test actors, which is not a system I want to rely
on. Fixed in three places:
- `template.json`'s default is `baseline`, not `hominid`;
- `buildActor` now **writes the profile explicitly** from the content spec rather
than leaning on the template default, so a packed actor always carries a real one;
- `check-rules` fails the build if any `speciesProfile` in `template.json` is not a
key of `locationTables`. Verified by reintroducing "hominid": the build stops with
`speciesProfile "hominid" is not a species (have: baseline, vesh, cadence)`.
The actor packs were deleted and rebuilt from clean, since the stale value was baked
into the stored documents rather than coming from anything still in the source.
Verified: ten actors, zero invalid profiles.
# Round 24 — a fight, and what the sheet knew about it. 10 Aug 2026
Four agents against a marksman and two contractors, 25 m closing to contact. Real
`Combat` document, armour counted once, initiative re-rolled each round. Agents took
it in four rounds, three standing; Dunn went down in round three.
The sheet was then read against the fight log, and it agreed on everything it
covered — Ceyla's left leg at 4/6 showing `hurt` on the figure, Dunn's head at 13/5
showing `destroyed`, his bleed on the wound bar, armour, reaction, MOV. What it
covered was the problem.
`R-174` **Only legs and arms ever did anything.** The location-effect table is
localised prose for nine anatomical kinds and the derived state implemented two of
them. Three of the remaining seven were live rules doing nothing:
| Kind | The text promises | It applied |
|---|---|---|
| `head` | "Unconscious immediately" | prone, and nothing else — **the agent stayed up** |
| `ridge` (Vesh) | "−30% to all Perception" | **nothing** |
| `unit` (Cadence) | "each disabled body costs the whole person −10% to everything" | **nothing at all** |
The Cadence one is the serious one: losing bodies is that species' entire mechanical
identity, and it was costing them nothing. A Cadence was strictly better than a
Baseline — same hit points spread over five bodies with no vital location to hit and
no penalty for losing four of them.
Found by shooting Dunn in the head and noticing he was still standing.
**Fixed the same way the Major Wounds were.** `locationEffectsFor(locations)` in
`rules.mjs` turns each kind into numbers; `prepareDerivedData` folds it together with
`woundEffectsFor` rather than computing anything inline. The two stack, because a
shattered leg and a concussion are separate facts about the same body.
Verified live:
```
leg destroyed phys -30 MOV 1
arm destroyed manip -30
HEAD destroyed unconscious=true down=true
vesh RIDGE opened percep -30
cadence 1 body lost all -10
cadence 3 bodies lost all -30 → a 60% skill rolls at 30%
leg gone + concussed all -20, phys -30, MOV 1 ← both tables stacking
```
Guard extended to 18 rules and 63 formulas, including every location kind the three
species tables use, so a kind cannot be added to a species without a consequence.
**Still prose, deliberately:** `vital` says "knocked down, and a Stamina roll or
unconscious" — the knockdown applies, the Stamina roll is left to the GM, which is
the right place for it.
# Round 25 — a mixed team, and the banner that was only half looking. 10 Aug 2026
Ceyla (Baseline), a generated Vesh and a generated Cadence against three Cleaners,
25 m closing to contact. Chosen so the location effects wired in round 24 would be
exercised by real dice rather than by me setting fields.
They were. The Cadence took a body and the sheet reported
`all-10 · 1 body lost`; a Cleaner's arm was disabled and his next shot went out at
**5%** — 45 less 30 for the arm, less 10 for range. A marksman's leg went and he
finished the fight at MOV 5 with `phys -30`. Team won in three rounds without a
casualty.
`R-175` **The Skills-tab banner was only watching one of the two sources.** It keyed
off `wounds.length`, which counts Major Wounds. Lost locations now produce identical
penalties and were invisible on that tab:
- a Cadence down two bodies sat at **−20% to everything** and the skills page said
nothing;
- a Vesh with an opened ridge and a dead grasp limb was at **−30% Manipulation and
−30% Perception**, likewise silent.
This is precisely the defect the banner was built to fix, for the other half of the
system. It now reads from a single `impaired` context built from the derived state,
so it shows whatever is actually modifying rolls, whatever produced it:
```
Cadence, 2 bodies lost
WOUNDED [First body ✕] [Fifth body ✕] all rolls -20%
Vesh, ridge destroyed and grasp limb gone
WOUNDED [Grasp limb] [Sensory ridge ✕] Manipulation -30% · Perception -30%
Cadence, bodies lost AND concussed and bleeding
WOUNDED [Concussed] [Bleeding] [First body ✕] [Fifth body ✕]
all rolls -40% · bleeding 1/round
Ceyla, head destroyed
UNCONSCIOUS [Head ✕]
Ceyla, unhurt
(no banner)
```
The headline changes with the state — **Wounded**, **Out of the fight**, or
**Unconscious** — so the worst news is the first word rather than something to be
inferred from a list of chips.
# Round 26 — Coherence in a fight, and a rule that had never once applied. 10 Aug 2026
Four agents against a Revenant, a marksman and a contractor, opening at 40 m so the
range ladder had work to do. Built around the two things dice had never exercised:
Ceyla firing the Redaction lance, which costs Coherence a shot, and Rakhi spending a
round aiming.
Both worked. Aim paid out — `Rakhi 21(+2)` in round two, one round after she spent
her action. Coherence ran **10 → 5** across five shots and left Ceyla **Loose** at
the end of the fight, which is the design's whole argument: the tool that solves the
case is the tool that costs you. And the stacking held up — the marksman took a
disabled arm and then a manipulation Major Wound and finished the fight shooting at
**1%**: 55, less 10 for range, less 60 for two −30s.
`R-176` **Encumbrance's skill penalty had never been applied to anything.**
`encumbranceFrom` has returned a `skillPenalty` since R-23. `prepareDerivedData`
stored it, the sheet displayed it, the rules journal tabulated it, and no roll ever
read it. Rakhi has been 20 kg over her free carry all session, carrying a printed
−10%, and rolling at full.
The comment above the modifier chain in `skillRoll` says gear bonuses "sit outside
the ±40% circumstance cap **alongside encumbrance and coherence**" — so it was
always meant to be there. Coherence was wired; encumbrance was not.
Now applied at the same choke point. **Scoped to physical, melee, ranged and
manipulation** — a heavy pack does not make you worse at Knowledge, and a rule that
says otherwise is one nobody will use at the table. That is a decision rather than a
fix, and the rules journal now states the scope explicitly so book and code agree.
Verified: Rakhi's rifle 47% → **37%** with the card reading `Overloaded: -10%`, and
her Navigate untouched at 31%.
`R-177` **The banner was watching two of four sources.** Rounds 24 and 25 taught this
lesson twice; here it was a third time. Four things modify rolls through
`skillRoll` — Major Wounds, lost locations, Coherence and load — and the banner knew
about the first two. Ceyla at Coherence 5 was losing 5% Communication with nothing on
the Skills tab; Rakhi was overloaded and silent.
All four now feed one `impaired` context, and **totals sum per category** rather than
listing each source, so a concussion and an Unmoored band read as one figure:
```
Rakhi, overloaded
WOUNDED [Overloaded] Physical -10% · MOV 8
Ceyla, Unmoored + concussed + arm destroyed
WOUNDED [Concussed] [Right arm ✕] [Unmoored]
all rolls -30% · Manipulation -30% · Communication -20%
```
Before the fix that last line read `all rolls -20% · all rolls -10%`.
**The pattern, stated plainly.** Four rounds running, the defect has not been a wrong
rule but an incomplete reader of one: Major Wounds computed and unapplied, locations
applied for two kinds of nine, the banner watching one source then two, encumbrance
computed and never read. The guard now covers 19 rules and 69 formulas, but it checks
*formulas*, not whether anything consumes them. **A rule that is computed and never
read is the failure mode this project actually has**, and it is worth a dedicated
check: every value `prepareDerivedData` produces should have at least one reader.
# Round 27 — the callout boxes, the range picker, and what a strong person can lift. 10 Aug 2026
`R-178` **Dialogs looked like a different application.** Every callout — attack
options, skill rolls, the reaction rail, the generators — rendered in Foundry's
default parchment with a serif face, sitting on top of a dark blue sheet in
Michroma and Helvetica. `rbDialogClass()` now returns a themed subclass that tags
every system dialog with `ringbrp-window`, and that class was added to the selector
that defines the design tokens, so dialogs inherit the sheet's palette rather than
carrying a copy of it.
Verified by computed style, not by eye:
```
window bg rgb(16, 26, 110) — identical to the sheet's --rb-deep
header rgb(12, 18, 91), yellow text, Michroma
content Helvetica Neue, --rb-white on --rb-deep
select --rb-panel ground, --rb-line border
Roll button --rb-yellow on --rb-void (yellow means the thing that commits)
labels uppercase 10px in --rb-muted
```
`R-179` **The range field asked for arithmetic the weapon had already done.** The
dialog printed "Bands: 20 / 60 / 180 m" and then a number box, so the player read
the bands, decided which one they were in, and typed a distance for the system to
convert back into a band. It is now a dropdown of the bands themselves:
```
Short 1–20 m +0%
Medium 21–60 m -10%
Long 61–180 m -30%
Extreme 181–360 m -50%
Beyond range over 360 m out of range
```
Each option carries a distance inside its band rather than a modifier, so
`rangeBandFrom` remains the single authority on what a range costs — the dropdown
chooses a distance, it does not duplicate the ladder.
`R-180` **Free carry ignored how big you are.** It was STR in kilogrammes. Rakhi at
STR 18 and MAS 19 — a large, powerful woman — had a free carry of **18 kg** and was
twenty kilos over while carrying 38 kg, which is an ordinary soldier's fighting
load. A rule that treats a realistic pack as an emergency is one the table quietly
drops.
Now **STR + MAS**, the same pair that sets the damage modifier, so a single physical
axis decides both how hard you hit and how much you can shoulder.
| | STR + MAS | free | carrying | over, before → after |
|---|---:|---:|---:|---|
| Ceyla | 12 + 13 | 25 | 15.4 | 3.4 → **0** |
| Dunn | 9 + 10 | 19 | 8.2 | 0 → 0 |
| Rakhi | 18 + 19 | **37** | 38 | 20.0 → **1.0** |
| Tomas | 13 + 12 | 25 | 7.2 | 0 → 0 |
Rakhi keeps a cost for her load — 1 kg over is −0.1 Reaction, so she prints 14.9
rather than 15 — which is the granular rule doing exactly what it was written for:
a heavy pack is felt, it is not a catastrophe. Rejected `(STR+MAS)×1.5`, which gave
her 56 kg and would have made encumbrance unreachable, and `STR×2`, which ignores
mass — the thing the owner specifically asked to count.
The rules journal states the new formula, and the guard covers it.
# Round 28 — the subsystem that was only ever bookkeeping. 10 Aug 2026
`R-181` **Defending did not exist.** `system.combat.defencesThisRound`,
`defenceTypeLock` and `secondAttackUsed` are declared in `template.json`, written
into every packed actor by `buildActor`, and reset every round by the combat hook.
Nothing ever incremented them, nothing rolled a defence, and no control existed
anywhere in the interface. The rules journal — which I generated — promised "one
offensive action, plus **as many defensive reactions as you like at an escalating
penalty**".
So the escalation was the only part of the rule that had ever been built, and it was
being applied to a counter nothing could raise. This is the same failure as the last
four rounds at its largest scale yet: a whole combat subsystem present as
bookkeeping for an event that could not happen.
**Now implemented.** `defencePenaltyFor` and `defenceTypeAllowed` in `rules.mjs`;
`defend(actor, {type, weapon})` rolls through the same `skillRoll` choke point, so a
defence is subject to wounds, coherence and load like everything else; a Dodge /
Parry control on the Combat tab that shows what the next one will cost.
| Defence this round | Modifier |
|---|---:|
| 1st | free |
| 2nd | −30% |
| 3rd | −60% |
| 4th | −90% |
Verified end to end — four attacks on Dunn in one round:
```
defence 1: no penalty → 49% failure
defence 2: -30% → 19% failure
defence 3: -60% → 1% failure (the 1% floor holding)
defence 4: -90% → 1% failure
asked to parry after dodging → "dodge" (the type lock)
round tick → 0 defences, lock cleared
sheet: "DEFEND DODGE PARRY first is free"
→ "DEFEND DODGE PARRY 1 this round · next at -30%"
```
**Played.** Four agents against three contractors at 10 m closing to contact, with
every agent defending and NPCs taking only their free defence. Agents won in seven
rounds **without a casualty** — considerably softer than the same fight without
defences, which is the point: a defence is free the first time and the rule makes
standing in front of three people possible and stupid.
The escalation never fired in that fight, because with seven combatants nobody was
attacked twice in a round. It needed a forced test to exercise, which is worth
remembering — **a rule that only bites in a corner will not be exercised by ordinary
play, and that is exactly how the last five defects survived.**
The journal now documents defending, generated from `defencePenaltyFor` so the table
in the book cannot drift from the ladder in the code. Guard at 22 rules and 77
formulas.
# Round 29 — weight on the kit, and a dice set that reports the drift. 10 Aug 2026
`R-182` **Weapons and armour did not show their weight.** Gear did; the two
categories that account for most of a load did not — so a player deciding what to
drop could see that a notebook weighs 0.3 kg and not that the rifle weighs 3.4.
Weapons gained a WEIGHT column, armour gained the figure beside its points, both
right-aligned with tabular figures so a kit list scans:
```
WEAPON | DAMAGE | RANGE | AMMO | WEIGHT
Marksman rifle | 1d10+2 | 20/60/180 m | 8/10 | 3.4 kg
Entry bar | 1d10 | — | — | 3 kg
Breaching suit 7 pts 11 kg
```
That matters more now that free carry is STR + MAS and the encumbrance penalty
actually applies: Rakhi is 1 kg over 37, and the sheet finally shows her which
kilogramme to put down.
`R-183` **A dice set for Dice So Nice, and it is the setting rather than a skin.**
The agency keeps a filing system and the game is about that filing system being
wrong. So the dice carry a ruled registry grid — and **the colourset is chosen per
roll from the roller's Coherence**, so the dice report the one number the players
are trying not to look at:
| Coherence | Set | What the die looks like |
|---|---|---|
| 10–8 | Anchored | true rule, lemon numerals on ultramarine, metal |
| 7–5 | Loose | the same grid, dimmer, plastic |
| 4–3 | Adrift | the rule has begun to wander and grain shows through |
| 2–1 | Unmoored | torn registry, coral numerals, glass |
| 0 | Displaced | no grid at all — fragments of rule going nowhere |
Plus an **Agency issue** set in brushed plate for anything that is not a person.
Four textures, generated rather than drawn, by `tools/make-dice-textures.mjs` —
raw RGBA encoded to PNG through `node:zlib` with a small built-in encoder, so the
repo gains no image dependency and a rebuild is byte-identical. All four tile
cleanly, which matters on a d20.
Verified positively rather than by absence of error, since Dice So Nice falls back
silently on an unknown colourset: `dice3d.exports.TEXTURELIST` holds all four
textures, `dice3d.exports.COLORSETS` all six sets, and
`getLoadedDiceSystems()` reports "The Custodians, 7 presets" — d100 and d10 labelled
as the percentile pair, plus d4/d6/d8/d12/d20. Every roll the system makes is
dressed: skills, the reaction die, damage, hit location and the Major Wound d10.
`R-184` **A deprecation warning of my own making, removed.** The damage-button
handler registered both `renderChatMessageHTML` and `renderChatMessage` for
compatibility, and v13+ warns on every single chat message when the old name is
bound. It now picks the new name when the v13 chat class is present.
# Round 30 — art, a catalogue that spans centuries, and the Quiet Arts. 10 Aug 2026
`R-185` **Icons rebuilt with depth.** A glyph was one silhouette; it is now up to
three layers — mass in near-white, **detail** in a darker tone (the grip and sight on
a pistol, the fuller on a blade, the lens housing on a scanner), and an accent in the
rim colour. The detail layer is the revision: it is what makes a flintlock read as a
flintlock rather than a pistol-shaped blob at 32 px.
**Era is carried by the tile**, so a glance at an inventory dates a thing before the
name is read: a notched corner and muted ground for antique, a cut corner and a
scanline for recovered, and **a broken rim for anomalous** — the tile does not close
on things that should not exist. 141 icons: 99 modern, 23 antique, 10 recovered,
9 anomalous.
`R-186` **The catalogue spans time.** Weapons 18 → 30, armour 10 → 13, gear 26 → 45,
filed into era sub-folders on install rather than one alphabetical column:
```
Weapons Armour Gear & Devices
└ Antique 6 └ Antique 2 └ Antique 8
└ Agency issue 21 └ Agency 10 └ Agency issue 29
└ Recovered 3 └ Recovered 1 └ Recovered 6
└ Anomalous 2
```
Flintlock, musket, crossbow, duelling sword, boarding axe, brigandine, oil lantern,
tallow candles, brass compass, pocket watch, chirurgeon's roll, wax tablet. Gauss
sidearm and lance-rifle, monofilament blade, survey drone, optic visor, data wafer,
stasis cannister, sealed hardsuit. And two that should not be in a kit list at all:
a **threshold key** that opens a crossing a hand's width, and **the quiet ledger**,
which writes down what happened — sometimes before it has.
`R-187` **THE QUIET ARTS.** The setting already contained the answer, so the powers
are not an addition to it. Crossing is an *induced filing error* — you persuade the
record you are somewhere else. **An Art is the same trick performed small and on
purpose: a misfiling committed deliberately.** There are therefore no fireballs;
every Art is a sentence about the record.
| Art | It says | Ordinary | Pushed |
|---|---|---:|---:|
| **Thread** | what was together is still together | −1 | −2 |
| **Echo** | *when* is loosely filed | −1 | −2 |
| **Hollow** | this is not filed here | −1 | −3 |
| **Witness** | the record agrees you belong | −1 | −2 |
| **Unmake** | this was not present | −2 | −4 |
| **Ledger** | read the record — and amend it | −2 | −4 |
Three design decisions worth recording:
**No new resource.** The cost is Coherence, because using an Art *is* doing something
that only makes sense somewhere else — precisely what transposed materiel already
charges for. It is levied whether or not the roll succeeds, since the misfiling is
committed the moment you try.
**Coherence penalises the Arts.** They run through the same `skillRoll` choke point
as everything else, so the further an agent has drifted the worse they are at the
thing that made them drift. Verified: Echo at 30% rolls 30 / 25 / 20 at Coherence
10 / 4 / 1, and at 0 the attempt is refused — there is nothing left to spend.
**A fumble is not a failure.** The misfiling *sticks*: something small and true about
the world is now different and the GM never says which. A story hook with a
mechanical anchor.
`R-188` **They only ever arrive from crossings**, per the owner's decision. There is
no path in but `grantPower()`: not taught, not bought, not chosen at creation, not
offered by the generator. Verified — a freshly generated character has none, and a
second grant of the same Art is refused. Each carries an `origin` line naming the
crossing that left it, printed on the sheet:
```
Echo 30% −1 [USE] [PUSH −2]
When is loosely filed.
Left by: Ilfracombe, the second crossing. She came back eleven minutes before she left.
```
New Item type `power`, its own compendium, an Arts tab, and a rules-journal page
generated from `ARTS` and `powerCostFor` so the book cannot drift. Guard at 24 rules
and 85 formulas — it caught both new rules arriving without coverage and stopped the
build until they had it.
`R-189` **The return is now a roll, and it is the only door the Arts come through.**
`returnCrossing()` described the modifiers and nothing ever resolved them — the GM
rolled it themselves, so "a crossing that goes wrong" was not an event the system
could observe. `crossBack(actor, {aimedByAnother, aimedBy})` resolves it through the
usual choke point.
| Return | What happens | Coherence | An Art? |
|---|---|---:|---|
| Success or better | home, where and when you meant | — | no |
| **Failure** | you arrive near your target, not at it | −1 | only if already Unmoored |
| **Fumble** | you come back, and not the same | −2 | **always** |
**R-56 is enforced in code rather than by convention.** There is no branch that
leaves an agent across: `setAcross(false)` runs on every outcome. Verified over
twenty returns by an Unmoored agent rolling alone — never stranded once.
**You do not choose the Art, and the pool darkens as you come apart:**
| Coherence when it went wrong | What can arrive |
|---|---|
| 8+ Anchored or Loose | Thread, Echo, Witness |
| 4–3 Adrift | Hollow, Thread, Echo |
| 2 and below Unmoored | **Unmake, Ledger, Hollow** |
Which is the trap the setting has been laying since the Coherence track was written:
**the agents most likely to come back with an Art are the ones least able to afford
using it.** Verified live — an anchored fumble produced Witness at 30%; an Unmoored
failure produced Unmake.
`R-190` **Three of the four pregens could not perform the job's defining act.**
Transposition sat at its printed base of 1% for Ceyla, Rakhi and Tomas; only Dunn had
it at 68. Every return would have failed, making "you arrive near your target" the
normal experience of being a Custodian and quietly reducing the crossing rules to
scenery.
Defensible as a design — one specialist aims for the team, and `aimedByAnother`
exists precisely for that — but not at 1%: these are people who cross for a living.
Ceyla 32, Rakhi 28, Tomas 35, Dunn unchanged at 68 as the field lead. The team can
now get itself home, and is markedly better off doing it through Dunn.
Found only because `crossBack` made the roll real. A skill nothing rolls is a skill
nobody notices is wrong — the same shape as R-176 and its four predecessors.
`R-191` **A crossing panel on the case file.** The sheet had a single `toggle-across`
flag-flip, which is right for going out — the brief names a place and a time and the
margin is the case's first problem — and wrong for coming back, because coming back
is a roll.
The panel now shows the whole decision at a glance while the team is across:
```
CROSSING [ BRING THEM HOME ]
AGENT | SKILL | COH | ALONE |
Ceyla | 32% | 10 | 32% | [RETURN] [AIMED]
Dunn | 68% | 10 | 68% | [RETURN] [AIMED]
Rakhi | 28% | 2 | 1% | [RETURN] [AIMED]
Tomas | 35% | 10 | 35% | [RETURN] [AIMED]
```
The **ALONE** column is the argument the panel exists to make: Rakhi, Unmoored at
Coherence 2, aims her own way home at **1%** — 28 less the −30 the band imposes,
floored. AIMED routes her through the best crosser at no penalty. Nobody has to know
the rule to see the cost.
`R-192` **`crossBack` no longer ends the case.** It cleared `across` on the case file,
so the first agent through the door marked the whole team home while the rest were
still standing in it. It now returns only the agent it was called for, and the caller
decides when the last one is back — which is what "Bring them home" does, looping
every member through the best aimer and then clearing the flag.
Verified end to end: Dunn aimed for the team, three came home at a cost of 1
Coherence apiece for rough returns, and **Rakhi failed hers while Unmoored and came
back with Hollow** — the dark pool, arriving exactly as designed, on the agent least
able to afford it.
# Round 31 — a full case, played end to end. 10 Aug 2026
Played as the team's **second** crossing, because Arts only arrive from crossings
that go wrong: Rakhi carried the Hollow she came back with from the first Ilfracombe
return. Brief → cross → four phases → first contact → an Art used → a fight →
return → debrief → end of case.
**What the case did.** The destination is *rumoured*, so every phase is Difficult:
Tradecraft 71 halves to 35 before the terrain modifier. Three of four phases failed
and the team reached the cove on **Containment 7, Resources 2**. Rakhi hollowed the
regulator case to get it past a watched approach — Hollow 26%, success, −1 Coherence.
Two contractors at the cove went down in two rounds. Dunn aimed everyone home at 68%
and took the only rough return himself. Debrief restored the team to Anchored.
That is the loop working: the pressure came from the tracks and the Coherence, not
from the dice being unkind.
`R-193` **First contact could not be made with a group nobody had met.**
`standingFor` returned `null` when a group had no standing entry, and
`resolveContact` bailed on null — so the *first* contact with a new group was the one
thing first contact could not do. Every stage silently returned nothing. It had gone
unnoticed because the shipped case file ships with a standing already on it; the bug
only appears with a group the team encounters for the first time, which is the normal
case in play and never once in testing.
`ensureStanding()` creates the entry on demand. Verified against a wiped standings
list: signal → success, read → failure, offer → success, standing 2 and impression 2
recorded.
`R-194` **The skills the job most uses could never improve.** The experience tick in
`skillRoll` fires only when a `skillItem` is passed. Every internal caller — case
phases, First Aid, Medicine, defences, and the crossing itself — passes a
`skillFamilyId` and no item. So Transposition, Tradecraft, Dodge and First Aid were
structurally incapable of earning a tick, and `endOfCase` reported "0 skills ticked"
for the entire team after a full case.
`skillRoll` now resolves the item from the family when the caller has not handed one
over. Verified: Ceyla ticked Dodge from a defence, Dunn ticked Transposition from a
return, and at end of case **Transposition improved 68 → 69**.
Both defects are the same shape as the run since R-176 — a rule that exists but that
nothing reaches — and both were found by *playing the case* rather than by testing a
subsystem. Two harness errors of mine on the way, recorded so they are not re-found
as bugs: the contact stages are `signal` / `read` / `offer`, not approach/pidgin; and
combat rolls driven through `resolveBands` directly bypass `skillRoll`, so they
legitimately do not tick.
**Still open from this case.** Nobody on the team has Fast Talk above its printed
base of 5, so the Cover Identity pack's +20% lands on a 25% skill. Tomas is the
talker on Persuade 44 and Language (Sign) 42, which works — but the Cover Work
talent, which exists, remains close to dead on the pregens.
`R-195` **Cover Work moved onto Persuade.** The talent granted +20% Fast Talk, and
nobody in the cast has Fast Talk above its printed base of 5 — so an agency training
talent held by the Case Officer was worth +20% on a 25% skill and effectively did
nothing. This is the surviving half of the "Cover Work / Fast Talk" problem the
generator already guards against for generated characters: the generator grants the
skill a talent modifies, but the hand-written pregens were never revisited.
It now grants **+20% Persuade**, which Ceyla has at 41 — and which feeds two of the
three contact stages, `signal` and `offer`, so the talent now bears on the work it
is named for.
Verified live: Persuade 41% rolls at **61%** with the chat card reading
`Cover Work: +20%`.
**Left alone deliberately:** the Cover identity pack (gear) still grants +20% Fast
Talk and has the same dead-bonus problem. It is a separate item and the instruction
was specific to the talent.
`R-196` **The Cover identity pack moved onto Persuade too.** Same dead bonus as the
talent: +20% to a Fast Talk nobody trains. Nothing in the content now hangs a bonus
on `fast_talk` at all.
Both work, and — worth recording — **the shipped party does not stack them.** The
talent sits on Ceyla, the Case Officer; the pack is in Tomas's kit, and Tomas is the
talker. As issued:
```
Ceyla · Cover Work talent 41% → 61% "Cover Work: +20%"
Tomas · Cover identity pack 44% → 64% "Cover identity pack: +20%"
```
Stacking is possible and the engine handles it cleanly, labelling both sources:
```
Ceyla · talent AND pack 41% → 81% "Cover identity pack + Cover Work: +40%"
```
**Flagged rather than changed:** +40% is a large bonus and gear bonuses sit outside
the ±40% circumstance cap, so an agent with a full legend rolls at 81% on a base of
41. That is arguably correct — trained cover officer plus a complete documentary
identity should be very good at being believed — but it is the largest single-skill
swing in the game and it is available by moving one item between two members of the
same team. If it wants trimming, the pack at +10 is the lever; the talent is the part
that ought to be worth the most.
`R-197` **Cover identity pack trimmed to +10.** The stacked total was the largest
single-skill swing in the game — 41 → 81 — and reachable by handing one item to the
agent who has the training. At +10 the ceiling is 71, and the ordering now says the
right thing: **the training is worth twice the paperwork.**
```
Ceyla · Cover Work talent 41% → 61% +20%
Tomas · Cover identity pack 44% → 54% +10%
Ceyla · talent AND pack 41% → 71% +30%
```
The item description carries the reasoning so a GM reading the sheet knows why it is
the smaller number: *"Paper helps; it is the training that sells it."*
# Round 32 — case three, and the Arts in anger. 10 Aug 2026
Played as the team's **third** crossing, with two Arts already carried: Rakhi's
Hollow from Ilfracombe and a Thread that followed Dunn back from Steepholm. They
started drifted — Coherence 9 / 8 / 7 / 6 — because they have been out twice.
**What happened.** Dunn threaded the missing team's beacon before setting out and
**failed** — and paid the Coherence anyway, dropping to 5. Two of four phases came
good; the cliff path did not; they reached the cove on Containment 9, Resources 3.
Rakhi tried to hollow the boathouse door and **pushed it** to cover herself as well,
failed, and paid all three: Coherence 7 → 4, **Adrift**, standing in the open. The
fight took four rounds and nobody was hurt. Dunn aimed everyone home; two rough
returns; the debrief pulled them back to 10 / 7 / 7 / 9.
Nothing followed anybody home this time, which is correct — no return was fumbled,
and the two failures were rolled by Anchored agents. **The Arts are meant to be
rare, and the gate held.**
`R-198` **An Art could never improve.** The `power` item carries an
`experienceTicked` field. `usePower` never set it, and `endOfCase` filters on
`i.type === "skill"`, so it was never read either — a field with no writer and no
reader, which is this project's signature defect in its purest form.
The consequence was worse than cosmetic. An Art arrives at roughly POW × 2 — **22%
for Dunn, 26% for Rakhi** — and charges Coherence whether or not it works. Fixed
forever at that rating, it is not a temptation, it is a trap: both Arts used in this
case failed and cost 4 Coherence between them for nothing.
Now a successful use ticks it and `endOfCase` improves it on the same footing as a
skill. Verified: Hollow ticked on its second attempt and improved **26% → 29%**.
Modelled over ten cases with one success each, a Hollow that arrived at 26 reaches
roughly **59%** — so the Art you keep using is the one that becomes reliable, and
the risk pays back across a campaign instead of never.
That also repairs the fiction. "The crossing left this in you" now has a second half:
you learn what to do with it.
# Round 33 — a mission framework, and two agents taking it apart. 10 Aug 2026
Built `tools/mission-tables.mjs` (content) and `tools/mission.mjs` (composer),
generating a full case: brief, truth, a named GAP between them, opposition, cast,
phases, clock, Coherence hooks, timeline, debrief question and failure state, as
five journal pages plus a playable case-file Actor. Wired into Foundry as
`generateMissionCase()` and a GM dialog.
Then handed it to two subagents: one running it as a GM deciding whether it is
runnable on Thursday, one auditing whether every field the generator emits actually
connects to the engine. Both came back with more than I expected.
## What the GM agent found
**89.3% of missions contained a defect a GM would have to patch before running.**
The cause was structural: **the composer treated every table as independent when
roughly half of them depend on the anomaly.**
`R-199` **The brief's objective and the truth's "how it closes" agreed 24.9% of the
time.** Drawn from unrelated tables. A case about a duplicated person ordered the
team to *"Recover the object"*; 12.8% ordered them to find a first team that does not
exist. Worse than wrong: the mission's own `gap` field names a *different*
contradiction, so a GM could not tell the defect from the design.
**Fixed by making the anomaly the parent key.** Every anomaly now carries
`fits: { resolutions, gaps, adversaries, classes, placeKinds }` and the composer
filters before picking. That single change also killed the Hollow-world cases whose
containment note read *"the town has noticed"* (there is no town — that is the
classification), the `not_hostile` gap applied to an eleven-minute time loop
(*"It runs from them at the first opportunity"* — a loop cannot run), and
`already_contained` on the inheritance anomaly, whose truth is that the family have
held it competently for four generations.
`R-200` **No phase in which they did the thing they were sent to do.** The spine was
approach → search → extraction, and extraction's verb is *"getting out with it"* —
presuming the *it* had been obtained offscreen. A **Containment** phase is now
compulsory in every mission. The GM's note was exact: this is the difference between
a three-hour session and a ninety-minute one.
`R-201` **Half of all missions were arithmetically unfinishable.** Phases cost
1+2+2+2 = 7 against a starting Resources of 6 for every non-Near class. Resources are
now derived from the phases that were actually generated, plus slack scaled to the
classification. Verified over 500 seeds: **0 unfinishable budgets.**
`R-202` **String plumbing.** "has been open for since the spring" (8.8%); a timeline
row reading "since a date nobody will confirm ago" (28.9%); briefs starting
lowercase (21.1%); a hard-coded *"The second phase makes the true scale unmissable"*
printed under heading 3; the same anomaly sign used in three of four phases (100%);
duplicate cast names in a game about duplicates; and "eleven" appearing up to eight
times in one case, which turns the register into a running gag the bible forbids.
`DURATIONS` is split into spans and vague dates, the timeline is relative, signs
cycle, names dedupe. Verified over 500 seeds: **all five string defects at zero.**
Also cut: a human spokesman for a revenant or a duplicate — entities whose whole
design is that they are singular and do not negotiate — who then lied *"about how
many of them there are"* against a hard-coded count of 1.
## What the integration agent found
`R-203` **The journal ownership was inverted, and it leaked the plot.** In Foundry
`2` is OBSERVER, `0` is NONE and `-1` is INHERIT. I had written
`i === 0 ? 0 : -1` — setting the **brief** to NONE and the four secret pages to
INHERIT. A GM opening the entry so the table could read the brief would have
published the truth, the opposition, the phases and the pressure. Now `2` for the
brief and `0` for the rest, verified page by page.
`R-204` **No case file was ever the active one.** `activeCaseFile()` looks for a flag
that **nothing in the system ever set**, so it fell through to the first expedition
in the collection — the shipped ILFRACOMBE case. Every `setAcross`, `isAcross`,
`debrief` and Clean Scene on a generated case was operating on a different actor, and
the sheet's Across toggle read one case and wrote to another. Generated cases now set
the flag and clear it from any other.
`R-205` **The whole first-contact pillar was unreachable from generated content.**
`resolveContact` needs a `group` item on the case file; `generateMissionCase` created
the actor with no items at all. Three stages, standings, learned taboos, desires,
correct offer form and the reward effects the phase resolver already applies — all
dead on anything the generator produced. Four GROUPS added, each with a taboo, a
desire, an offer form and standing rewards, and the case is built with one embedded.
Verified live: signal → read → offer, standing 2.
`R-206` **`leg.difficulty` was written and never read.** The generator sets a
deliberate ramp and explains it in a comment; `resolveLegDialog` derived difficulty
from `destinationKnowledge` instead, so the brief said "phase 1 is Average" and the
phase table disagreed on the same document set. The resolver now honours an explicit
per-leg difficulty when one is given.
**Still open, and worth doing:** `crossingModifier` is printed in the brief as a
mechanical promise and never applied by `crossBack`; `salvageEra` is computed and
referenced nowhere at all; `leg.explored` is read by the resolver and written by
nothing; `journey.elapsedDays` never accumulates; `tools/generator.mjs` is orphaned
dead code duplicating the live THREATS table. Four more instances of the pattern.
## What the GM agent said was good, and worth protecting
The `ANOMALIES` table — *"each row is internally airtight, and each truth is a
genuine inversion of its brief rather than an escalation"* — and the `GAPS` names,
*"ten different session shapes, not ten flavours of one"*. The failure was never the
content. It was a composer that rolled dependent tables independently.
`R-207` **The four dangling values, cleared.** Each was the project's signature
defect — a value computed and never read — and each is now either wired or gone.
**`crossingModifier` was a promise the engine did not keep.** The generated brief
printed "return modifier for the classification: −20%" and `returnCrossing` computed
the penalty purely from the agent's Coherence band, so a **Wrong** world was no
harder to leave than a Near one. The case file now carries it and the return charges
it. Verified on a Wrong case:
```
Anchored, alone −20% world −20, coherence 0
Unmoored, alone −50% world −20, coherence −30
Unmoored, aimed −20% aiming removes the coherence part, not the world
```
That last line is the one worth having: another agent can steady *you*, but nobody
can make the place less strange.
**`salvageEra` was the cleanest dead field in the codebase** — computed by the
generator, referenced nowhere, not even rendered. It is now a mechanic:
`salvageFrom(caseFile)` draws from the catalogue by era, which items have carried
since the era rework. A Wrong world yields the Threshold key and the quiet ledger; a
Hollow one yields lanterns and brigandine. That is what crossing to other times is
*for*, and it was one line from existing.
**`leg.explored` was read and never written.** The resolver branches on it to choose
between "you already know this ground" and "you find a site", so every critical
reported a site. Ground the team has crossed is now marked explored: 0/5 → 2/5 after
two phases.
**`journey.elapsedDays` never accumulated.** Recorded per leg, displayed on the
sheet, never summed. Now 0 → 2 after two phases.
**`tools/generator.mjs` deleted.** Orphaned duplicate of the live THREATS table,
imported by nothing, and stale — its entries used the retired `weapons: [...]` shape
against the live `attacks: [{weapon, skill}]`. Checked before removing and again
after: all six threat tiers still generate, and all six mission adversary tiers and
NPC names still resolve.
## Play-through: SOFT TENANT (seed 2718)
Generated a case from the mission framework and ran it end to end — crossing, five
phases, first contact, a fight, the return, debrief, close. Four findings, all of
them the same species as everything before them.
**R-208 · `generateNPC` guarded one lookup out of three.** An unknown `species` fell
back to baseline; an unknown `threat` or `role` reached `TH.armour`/`R.label` and
threw. Callers got a TypeError instead of an NPC. All three now fall back, and the
substitution is announced rather than silent.
**R-209 · `hitLocationRoll` treated bad damage as zero damage.** `Number(damage) || 0`
meant a caller passing `undefined` got a hit that rolled a location, printed a chat
card, and took nothing off. A six-round fight ran with fourteen connecting blows and
no casualties, and nothing anywhere said a word — the fight *looked* right in the
log. Damage must now be finite or the hit is refused loudly. `majorWoundRoll` got the
same treatment: called positionally by mistake it rolled and applied a Major Wound to
nobody.
**R-210 · `endOfCase` was exported and called by nothing.** Documented in its own
comment as "the step that closes the episodic loop", wired to no button and no hook.
Every ticked skill and Art sat on the sheet forever: no character could improve
through play, and the Arts — which already had `experienceTicked` fixed once for
exactly this reason — still could not get better. Now a **Close the case** control on
the case file, guarded against being pressed while the team is still across. First
use: Rakhi's Hollow 26% → 31%.
**Salvage returned "Punch / kick".** The unarmed pseudo-weapon every character
carries was in the catalogue draw, so it turned up as loot lying in a filter house.
Salvage now requires an item with mass.
Not defects, recorded because play made them look like defects:
`crossBack` deliberately leaves `system.across` set — one agent returning does not
end a crossing the rest of the team is standing in, and "Bring them home" is what
clears it. Deep bleeding correctly refused an average First Aid and yielded to a
difficult one. Both behaved exactly as written.
## The establishment: 20 more postings
Five roles were never the whole agency — they were the five a field team fields.
Twenty more are now on the books, each with its own characteristic, core and
support skills, talent leaning, and a loadout containing at least one thing no
other posting is issued. Two are deliberately archaic (the Antiquities Officer
draws a duelling sword and a flintlock; the Night Warden carries an oil lantern
and a ward plate), because an agency this old has never got round to abolishing
anything.
Adding them exposed three faults.
**R-211 · the generator matched kit by guessing at its name.** `keyMatches` was
fuzzy substring matching against a hand-maintained alias table, and anything the
table had not heard of was dropped without a word. The catalogue names diverge
from the keys almost everywhere — `binocs` is "Field glasses", `rope` is "Line and
harness", `geiger` is "Radiation counter", `sealed_cannister` is "Stasis
cannister" — so a Stores Officer issued five things received one, and an Overwatch
Officer never got the rifle the posting is named for. Worse, `climbing_kit` was
mapped to "breaching charges" outright, so the Rigging Officer was handed
explosives instead of a harness and nothing looked wrong.
Items now carry their catalogue key in `flags.ringbrp.key`, stamped at pack build,
and the match is exact. The name pass survives only as a fallback for documents
built before the flag existed, and an unresolved kit key is now reported instead
of lost. All 25 postings receive their full loadout.
**R-212 · two weapons named a skill that does not exist.** The boarding axe and the
entrenching tool both declare `melee_weapon:axe`, and the catalogue had no such
skill, so neither could ever be rated above the 5% base — the generator would hand
a Rigging Officer an axe they were permanently incompetent with. Skill added; every
weapon in the catalogue now maps to a real skill, checked.
**`tools/check-kits.mjs`**, gated into the pack build alongside `check-rules`. Every
kit key must name a real catalogue item, every skill and talent reference must
resolve, no two postings may share a loadout, and it warns when a posting has
nothing issued to it alone.
## Sheet
**Characteristics were smaller than the skill ratings below them.** The eight
numbers the entire system derives from read as a caption under the ×5 chips. They
are now the dominant band on the sheet, ruled top and bottom.
**The marksmanship dropdown offered a choice with no stated consequence** — two
words, Reflex or Deliberate, and nothing anywhere on the sheet saying what either
buys. Both styles are now printed under the control with the chosen one picked
out, assembled from `RINGBRP.marksmanship` itself so the sheet cannot describe a
style differently from the way it rolls.
## One agent per posting, sheets checked
Generated a character in each of the twenty new postings and audited every sheet —
rendered DOM, not just the data. All twenty come out clean: eight non-zero
characteristics, both marksmanship rows, 53 skills, 2 talents, 8 hit locations and
a figure on the Combat tab, full loadout issued, no `undefined`, `NaN` or unrendered
Handlebars anywhere. Empty states read properly ("No Arts. They are not learned —
they arrive.").
Two faults found.
**R-213 · nobody had hands.** "Punch / kick" is in the weapon catalogue and was
issued to no character, pregen or NPC — every agent carries Brawl and had no item
to roll damage with, so an unarmed attack was unrollable for every character in the
game. It went unnoticed because every playtest so far happened to arm everyone.
Worse, the comment written into the salvage fix a few hours earlier asserted it was
"the unarmed pseudo-weapon every character has", which was simply untrue. Both the
generator and the pack builder now issue it to everyone.
**Salvaged crossing plate was standard issue for two desk postings.** At 10 AP it is
the strongest armour in the game — better than plate (8) and the sealed hardsuit (9)
— and the Materials Officer and Rigging Officer were both wearing it as routine kit,
making two support specialists the best-armoured people in the agency. Materials now
draws a stab vest, Rigging an impact vest. Salvaged plate is no longer anybody's
standard issue, which is as it should be: it is salvage.
Checked and not a fault: the Disposal Officer at 9 AP is in a sealed hardsuit, which
is the posting, and pays 12kg for it.
## A fight with four of the new postings
Overwatch, Rigging, Antiquities and Stores against three Dangerous adversaries —
chosen to exercise the anti-materiel rifle, the axe skill added yesterday, a
single-shot flintlock, and an agent whose only weapon is the unarmed attack. Twelve
rounds; the team lost, all four down, two adversaries still standing. That is a fair
result for a rifle, an axe, an archaic swordsman and an unarmed storekeeper against
three armed professionals.
Three faults, all found by playing rather than by reading.
**R-214 · every generated agent was an unlinked token.** `generateCharacter` never
set `prototypeToken.actorLink`, and Foundry defaults it to false. So an agent put on
the map took damage, wounds and Major Wounds on the token's private copy while the
player's character sheet stayed pristine — the sheet and the body were two different
people. The pack builder has always linked characters (`actorLink: type ===
"character"`); the runtime generator, written later, did not. Agents are now linked;
NPCs stay deliberately unlinked, because one statblock stands up four bodies and each
needs its own hit points. Verified at the end of the fight: every agent's token and
sheet agree, and the adversary's token reads 0 while its statblock still reads 12,
ready for the next body.
**R-215 · Reaction was re-rolled every round except the first.** `nextRound()` re-rolls,
which is the whole design — but Foundry does not roll initiative when a combat
starts, it waits for the GM to press Roll All. Nothing did. So round one ran with
every initiative null and the comparator falling through to its DEX tiebreak: the
opening round of every fight, the one where the shooting starts, was the single round
the Reaction system did not order. `startCombat()` now rolls it.
**Ten of the twenty new postings carried a weapon they had no skill for.** The
Antiquities Officer drew a flintlock and rolled it at 5%, which is what exposed it;
the Registry, Interview, Pathology, Recording, Access, Disposal, Translation,
Naturalist, Pursuit and Settlements Officers all had the same fault. Every posting's
support list now trains what it is issued, and `check-kits` fails the build if a role
is ever handed a weapon it cannot use. The flintlock is now 40%.
**Flagged, not changed — a critical attack is cancelled by an ordinary success.**
`defend()` never learns what it is defending against: `turnedAside` is true for any
critical, special or success, so a bare success parry stops a critical entirely. The
rules text as written supports this — a defence either works or it does not — so the
code is not wrong, but it sits badly against everything else the system says about
criticals (maximum damage, armour ignored entirely). In play a boarding-axe critical
was turned aside by a special parry and cost nothing. Grading defences by level
(a defence stops an attack of equal or lower level; a lesser defence only reduces it)
would make criticals matter, but it changes combat maths materially and is a design
decision, not a defect fix.
## Defences graded by level
The binary rule is gone. A defence now stops a blow of its own quality or worse
outright; against something better it still helps, but only by as much as it was
outclassed by.
landing rank = failure + (attack rank − defence rank)
vs critical vs special vs success vs failed
critical turned aside success special critical
special turned aside turned aside success special
success turned aside turned aside turned aside success
So only a critical stops a critical, a success parry shaves a critical to a special
rather than cancelling it, and a failed defence does nothing at all. The reduced
level is the result for everything downstream — a critical cut to a special no
longer ignores armour, it merely halves it.
`defenceOutcomeFor(attackLevel, defenceLevel)` and `levelRank()` live in `rules.mjs`
with the rest, carry ten spot-checks in `check-rules`, and the rules journal builds
its table by calling the function, so the book cannot describe it differently from
the way it resolves. 29 rules, 104 formulas.
**The standing blow.** A defence has to know what it is answering and the sheet's
Dodge and Parry buttons had no way of knowing — they were pressed in isolation.
Every attack roll now records what it scored; the defender picks it up; the defence
amends it to whatever survived. It is kept on the active Combat so it reaches every
client rather than only the machine that rolled, falls back to a module-level copy
when nobody started a combat, expires after two minutes, and is never offered to the
person who threw it.
**R-216 · the damage button ignored the attack entirely.** `_onRollDamage` called
`rollWeaponDamage` with no level at all, so it always rolled ordinary damage: a
critical got neither maximum damage nor its armour bypass, and the whole critical
rule existed only in the wording of the chat card. This is why grading defences was
not enough on its own. Damage now rolls at the level the blow actually landed at,
and a blow that was turned aside refuses to roll damage at all.
Played it: four senior agents against three Dangerous adversaries, twelve rounds,
the agents won. Every cell of the table came up in play, including the one that
prompted the change — Ganger's critical against Marek's success parry, cut to a
special and landing for 6 where it would previously have cost nothing — and its
mirror, a critical parry turning aside a special.
## Balance check on the graded defence
Played a standard field team — Field Lead, Containment, Technical, Physician —
against four Armed, and then ran the maths properly: 3000–4000 simulated fights per
configuration, driven by the shipped functions (`resolveBands`, `gradeRoll`,
`defencePenaltyFor`, `defenceOutcomeFor`, `armourAgainst`, `damageAfterArmour`)
rather than a reimplementation.
**Grading did not make combat more lethal.** My warning when I shipped it was wrong
and is withdrawn. Both sides lose defensive value equally, so it nets out:
4 trained agents vs binary graded Δ win
bystander x4 100.0% / 5.8r 100.0% / 5.7r +0.0
trained x4 100.0% / 7.8r 100.0% / 7.5r +0.0
armed x4 79.9% / 13.2r 80.7% / 12.2r +0.7
dangerous x3 35.0% / 12.8r 37.8% / 11.8r +2.8
anomalous x2 18.6% / 16.4r 20.6% / 15.2r +2.0
Agent casualties are unchanged (1.76 → 1.78 at Armed, 3.14 → 3.09 at Dangerous).
Fights are consistently about one round shorter. `DEFENCE_STEP` does not need
touching.
**R-217 · Dodge was 0% for most of the agency.** Dodge is the only skill whose base
is written as a formula — `dex*2` — and nothing in the system ever evaluated a
formula base. `base.value` stayed 0, `skillRating()` read `system.value` at face
value, and so any posting that did not separately train Dodge had **Dodge 0%**: the
game's universal defence, unavailable to ten of the twenty-five postings, and
missing its DEX×2 floor even for the fifteen that had it. This is what the balance
check was actually for, and it is worth far more than the grading change:
dodge 0% — as it actually shipped win 52.0% 2.77 of 4 agents lost
dodge 26% — DEX x 2 floor (fixed) win 66.5% 2.34 of 4 agents lost
dodge 47% — a posting that trains it win 79.7% 1.82 of 4 agents lost
`skillBaseFrom(formula, characteristics)` now lives in `rules.mjs` with five
spot-checks, is applied as a floor rather than a replacement in the skill item's
derived data, and deliberately parses rather than evals. Every posting now shows a
real Dodge: the untrained sit exactly at DEX×2, the trained above it, none at zero.
30 rules, 109 formulas.
**R-213, third time.** Generated NPCs still had no unarmed attack. The pack builder
and `generateCharacter` were both fixed; `generateNPC` builds its items on a
separate path and was missed, so four adversaries armed only with pistols stood
through an entire close-quarters fight doing nothing. Fixed there too, with the
Brawl skill to go with it.
**Two observations, not defects.** Threat tiers are steep — Dangerous x3 is winnable
about 38% of the time and costs three of four agents; Anomalous x2 is around 20%.
And an unarmed attack is 1d3 against armour that is routinely 2, so fists are close
to useless against anyone equipped: the tail of the played fight was four people
punching each other for five rounds to no effect. Both are arguably correct, but a
GM should know before pricing an encounter.
## Two more fights with the Dodge floor in
Standard field teams including postings that rely on the new DEX x 2 floor, against
Armed adversaries. First fight: a clean win, all three adversaries down, nobody
lost. Second: ten rounds, three of four adversaries down, two agents lost.
Neither fight happened to land a floor-rated dodge, which is too thin to conclude
anything from, so the effect was measured per incoming blow instead — 200,000 trials
at each rating, attacker at 48%, first defence of the round:
dodge 0% stopped 1.0% reduced 0.0% through in full 99.0%
dodge 18% stopped 14.7% reduced 3.1% through in full 82.2%
dodge 24% stopped 19.9% reduced 4.0% through in full 76.1%
dodge 30% stopped 24.8% reduced 5.2% through in full 70.0%
dodge 47% stopped 38.9% reduced 8.1% through in full 52.9%
dodge 58% stopped 47.8% reduced 10.1% through in full 42.0%
0% is what shipped; 18–30% is the floor; 47–58% is a posting that trains it. A
support agent went from taking 99% of blows at full effect to taking 70–82%. The
1.0% at dodge 0% is the 1% floor in `resolveBands` doing its job — a roll of 1 is
still a critical dodge.
The "reduced" column is the graded rule earning its keep: 3–10% of blows now land
degraded rather than either landing whole or vanishing.
Two harness faults, mine not the system's, recorded so the next play-through does
not repeat them. Picking a weapon by highest skill rating made adversaries punch
rather than shoot, because `generateNPC` gives Brawl a rating from the same range as
the weapon skills — a fight ran entirely at 1d3 against armour 2. Choosing by
expected damage after the target's armour fixes it. And once Punch / kick is always
available, a weapon-selector that only reloads when it finds nothing to swing will
never reload at all.
## Brawl is a fallback
`generateNPC` rated Brawl from the same range as the weapon skills, so an Armed
adversary was exactly as good with its fists as with the pistol on its hip — and any
sensible target-picker chose the fists, which then bounced off armour 2 for 1d3. A
whole fight ran that way.
`brawlFallbackFor(trained)` in `rules.mjs`: sixty per cent of the trained rating,
never below the skill's printed base of 25. Four spot-checks. 31 rules, 113 formulas.
tier weapon avg brawl avg
bystander — 25%
trained 39% 25%
armed 44% 28%
dangerous 61% 35%
anomalous 67% 67% <- attacks WITH Brawl
apex 79% 75% <- attacks WITH Brawl
**The trap in this one.** The block that issues the unarmed attack ran BEFORE the
attack packages, and the package loop will not overwrite a skill that already
exists. Lowering Brawl in place would therefore have crippled exactly the creatures
whose attack IS Brawl — the anomalous grasp and the apex — reducing a 67% attack to
40% and quietly gutting the top two threat tiers. The block now runs after the
packages and only fills Brawl in when nothing else has, so a grasp keeps its full
rating and only an adversary genuinely falling back on its fists takes the
reduction. The three roles that legitimately train Brawl — Containment, Night
Warden, Rigging — are likewise untouched.
Played it: four field agents against three Dangerous, ten rounds, three agents down
and two of three adversaries down before it came to a last two standing. **35 weapon
attacks, no fist attacks.** The fight opened on the graded rule doing precisely what
it was built for: Whitloe's rifle critical against Mirren's successful dodge, cut to
a special, still 10 to the chest, still fatal. Under the old binary rule that dodge
cancelled it outright.
## Stores, and saying what a posting is
**The catalogue is roughly twice the size.** Gear went from 45 entries to 93 — 137
items across gear, weapons and armour. The new stock is the small equipment an
agency this old actually issues: warrant cards, field dressings, marking chalk,
numbered seals, carbon duplicate books, a date stamp that occasionally stamps the
wrong date, cordon tape citing an Act that does not exist, and a vacuum flask
described in the file as the most requisitioned item in the department's history.
Spread across all four eras — tinderboxes and quill and ink horns, wound foam and
cold light strips, and five more transposed objects priced in Coherence including
the coin that comes back and the attendance register, which is right, and is not
always a list you want. 189 icons regenerated; every catalogue item has one.
**An agent now leaves creation properly equipped**, 16 to 21 items rather than 8:
- `STANDING_ISSUE` — anchor, handset, warrant card, dressings, lamp, chalk,
whistle, notebook. Every officer, every posting.
- The posting's own kit, expanded from five items to eight.
- `TIER_ISSUE` — seniority is issued more and trusted with more, from nothing at
Probationary up to seals, master keys, a duplicate book and a trauma unit for a
Case Officer.
- `PERSONAL_EFFECTS` — one thing that is theirs rather than the department's, which
is the reason the anchor works: an agent carrying nothing of their own has nothing
to be filed against.
Encumbrance holds: a Containment officer comes out at 26.1 kg against a 29 kg free
carry, and `stowOverload` already sheds the rest for anyone heavier.
**The role dropdown said nothing about the roles.** Twenty-five job titles, no
indication of what any of them did. Each posting now carries a `blurb`, and the
creation dialog paints a card as the posting is selected: what the job is, its key
characteristic, what it trains, and what it draws from stores. The names are read
from the compendium rather than derived from the ids, so the dialog cannot print
"Quill Ink" where the sheet says "Quill and ink horn" — which is exactly what the
first cut of it did.
## Transport, and the forward stores
**A `vehicle` item type.** A lorry is not a piece of kit and modelling it as gear
would have meant lying about it in the encumbrance sum, so vehicles are their own
Item type with their own schema: crew, seats, cargo, hull (its own hit points),
armour points (which protect the occupants), range, terrain, and the skill that
works it. Speed is a BAND — walking pace through to no travel time at all — because
the game measures distance in bands everywhere else and a figure in km/h would have
been the only number on the sheet nobody used. Vehicles carry `carried: false`, so a
van in the boot of the character sheet does not weigh the agent down.
Fourteen of them in their own compendium: the pool car, the Custodian van with the
cage bulkhead, a long-wheelbase utility, a containment lorry, a dispatch motorcycle,
a river launch (most of the department's older sites are drains), a light
helicopter, and — still on the inventory and never struck off — a hand cart and a
pony and trap. Forward of us: the flycycle, a cargo lifter, and a sealed crawler
with eleven days of its own air. And two that came back with us: the stepping frame,
and the carriage that is already parked outside wherever you were going to go.
**Twenty-five more items in stores.** Transport stores — tow line, jack and sand
plates, jerry can, spares roll, the vehicle log book that is the most audited
document in the department. Then the forward stores, which Registry will not issue
to a probationary officer: stepping discs, a stasis box, a field autodoc, impact
liner, shadow cloth, a descent harness, a sonic probe, a pressure tent, translator
pins, trauma ampoules that bring an agent back from zero once. And three things
filed further out still — a sealed sphere nothing has opened from outside, the
contentment tap the department has destroyed six of, and a maintenance log for a
structure nobody has located, still being written.
The catalogue is now **175 items**: 118 gear, 14 vehicles, 30 weapons, 13 armour —
103 modern, 26 antique, 34 future, 12 anomalous. 228 icons; every one is drawn.
## Hacking: three options, and a debate with Codex
Not built. Three options put to the designer to choose from, after two rounds
against Codex (gpt-5.6). Codex opened with three designs and picked The Access
Dossier; I objected on two grounds it could not have known — that the system already
has a standings subsystem with taboos, desires and per-tier rewards that an
"Authority" maps onto almost exactly, and that the Dossier models the break-in but
not the thing that actually frightens people about modern intrusion, which is access
that persists and detection that arrives weeks later. Codex conceded both, discarded
its own first option as "the rejected run structure reskinned", and moved its pick
to Borrowed Authority with the Dossier demoted to an acquisition procedure rather
than a second subsystem.
Full transcripts in the session scratchpad. Options as put to the designer:
1. CORDON & TRACE — staged intrusion against a rising Trace clock. Both of us
rejected this: a live infiltration with an alert meter is the cyberpunk run in a
grey coat, whatever the fiction on top.
2. THE ACCESS DOSSIER — the team assembles a case file across several scenes and
different skills, then one decisive roll buys exactly the declared objective.
Fast, collective, sharply bounded; weak on persistence, and its Posture numbers
need tightening or three generic items beat any target.
3. BORROWED AUTHORITY — access is a named credential with a scope, a Trust value, an
expiry and a red flag. In scope, it simply works. Out of scope, roll. The horror
is that Coherence can widen a scope because reality now remembers the person held
that duty — and below zero Trust, the agent's own records begin merging with the
identity they borrowed.
## Borrowed Authority, built
Option 3 with the dossier as acquisition, as chosen. There is no hacking skill and
nothing to break into.
**The rules** are in `rules.mjs` with nineteen spot-checks: `SCOPE_TIERS`,
`scopeStanceFor`, `scrutinyFor`, `authorityOutcomeFor`, `trustAfter`,
`dossierTrustFor`, `dossierScopeFor`, `widenScopeCostFor`, `mergeStageFor`.
40 rules, 132 formulas.
- **Inside scope there is no roll.** A facilities contractor opening a plant room is
Tuesday. 1 Trust, and it works.
- **One tier beyond is a stretch** — roll, at −20 per red flag you break. The system
is not testing whether you are clever, it is testing whether you behave like the
person whose name is on the record.
- **Two tiers beyond is impossible at any skill.** That is the rule that stops one
agent quietly owning every system in the setting.
- Trust: critical +1, special free, success −1, failure −1, **fumble burns it** and
puts a real person in the department's way. At 0 the account is flagged until a
scene rehabilitates it.
- **The dossier** is acquisition, not a second subsystem: three items, each a
separate scene with a different skill, at most one at a keyboard. Items make the
credential better, never the roll easier — which is what stops three generic
successes beating any target.
- **AI** is a posture on the target, not an opponent. A *gatekeeper* raises what
counts as routine; an *analyst* writes a new red flag onto the credential every
time it is stretched; an *asset* stands in for a dossier item.
- **Coherence** widens a scope, because reality can be made to remember the person
always held that duty. First costs 1, then 2, then 3. Push past what Trust will
carry and the two files converge a step, and nothing spends anything to converge
them back.
Played it end to end: a Registry, Interview and Access officer built the credential
for M. Hughes, facilities contractor; the Signals officer opened a ticket and read a
door schedule with no roll at all; failed to alter the schedule at two in the morning
because that is one of the things Hughes never does; was told flatly that a
contractor could never issue accounts; spent Coherence to make the switchroom his
remit, after which the same action needed no roll; ran the credential to 0 Trust,
found it flagged and refused, and had it talked back into service by the Interview
Officer. Coherence 10 to 7, and a circular now arrives at the agent's desk addressed
to M. Hughes.
**R-218 · one localisation key un-localised the entire system.** Shipping both
`RINGBRP.Authority.Merge` (a string) and `RINGBRP.Authority.Merge.0` made Foundry try
to assign a property to a string while expanding dotted keys. It throws inside the
loader and the loader abandons the WHOLE FILE — every sheet in the system silently
began printing raw keys, with no error a user would ever connect to the cause. The
leaf is renamed, and `tools/check-lang.mjs` now fails the build on any key that is
both a value and a prefix of another, and on duplicate keys, which `JSON.parse`
swallows.
## The case generator gets a door
**R-219 · `missionDialog()` was exported, complete, and reachable from nothing.** The
entire case generator — anomalies, gaps, adversaries, phases, seeds — existed behind a
console call. The Actor Directory offered "New Agent" and "New NPC" and no way to make
a case. It now sits with the other two generators, because a case IS an actor and that
is where actors are made.
**R-220 · a new case conscripted every character in the world.** The roster fell back
to `game.actors.filter(a => a.type === "character")`, which is defensible in a world
holding one party and absurd in a world holding several — a freshly generated case
here listed thirty-six agents. The fallback exists for a real reason (a case with no
roster cannot resolve a phase, and `resolveLegDialog` bails silently), so it is now
ordered by how good the guess is: player-assigned characters first, then whatever the
GM has selected on the canvas, and the whole world only as a last resort — and it says
so when it does. Verified: with four tokens selected, the new case has four members.
## SOFT VERGE, played with four new postings
Registry, Access, Disposal and Settlements — no role from the original five. The case
generated a location filed twice with the two filings drifting apart, which is a
Registry Officer's whole job, and a police cordon the team could not show a warrant to
because the staff's taboo forbids it.
They tried Borrowed Authority on the cordon: Registry failed the roll that establishes
what PC Aldiss is actually FOR, so the credential stayed at "read their own records",
which made signing four people through a STRETCH rather than routine — and it was
refused. That is the subsystem working exactly as designed; the dossier item that
matters is the one nobody rolled well on.
The case was won socially, not tactically. The Settlements Officer read the staff and
made the offer, standing 0 → 2, and they handed over the log — the only continuous
record of a place filed twice. Containment and Extraction both failed at 8% and 3%,
Resources hit 0, and everybody came home. Two skills improved at close.
**Balance note, not a defect.** The phase ladder gets brutal at the tail: difficult
plus escalating modifiers plus destitution put the last two phases at 8% and 3%. A
team that loses Resources early is not going to pass another phase, and the case
becomes about what they can still do socially. That reads as intended given the tone,
but a GM should know that Resources is the track that actually decides a case.
## Documentation pass
Audited every exported rule in `rules.mjs` against what the rulebook actually says,
and closed the gaps the audit found:
- **Dodge starts at DEX × 2** — newly evaluated, and stated nowhere. Now on the
fighting page, worked with a real number.
- **Encumbrance** — free carry is STR + SIZ, and which skill categories it penalises,
as a table generated from `ENCUMBERED_CATEGORIES`.
- **Bleeding** — rates and what stops each kind, from `BLEED`.
- **The merge ladder** — the five stages of your file converging with a borrowed one,
as a table rather than prose.
- **A new page, "The postings"** — all 25, each with its key characteristic and what
the job is, generated from `ROLES` in `ringbrp.mjs` (read out of the source, since
that module cannot be imported outside Foundry).
The rulebook is 9 pages, every page generated from the code it documents, and no
unresolved localisation key leaks into it. All packs rebuilt: 53 skills, 18 talents,
30 weapons, 13 armour, 119 gear, 14 vehicles, 6 Arts, 4 pregens, 6 NPCs, 2 tables,
the rulebook and the starter case. 231 icons and the four dice textures regenerated.
## IRON SIGNAL, played with Anchor / Pathology / Overwatch / Night Warden
A family removed from the world and not from the paperwork, with something using the
gap; the adversaries were themselves misfiled here generations ago and this is their
last stable ground. Good case. The team lost it.
Approach succeeded, then everything after failed. Contact with the staff opened on a
special and then failed twice, so the log never came across. Resources reached zero at
phase four. The case ends unclosed — the record was not put back — which the generator
already has an answer for: it followed somebody home, and that is next season's case.
**R-221 · the phase-lead fallback could never fire.** `bestIn("tradecraft") ??
bestIn("navigate")` — and every character carries every skill, because
`grantFullSkillList` puts them there, so `skillRating()` returns the printed base
rather than null. `bestIn("tradecraft")` therefore always found somebody and Navigate
was unreachable. A team of postings that train Navigate and not Tradecraft — and ten
of the twenty-five train neither, while eight train only Navigate — was led by
whoever had the highest UNTRAINED Tradecraft, which is the printed 5%. Measured on a
Pathology / Overwatch / Warden / Materials team: **the lead was 5%, and phase three
resolved at 1%.** With TRAINED meaning "above the printed base", the same team leads
at 34%. Fixed.
**And a correction to my own diagnosis.** I first read IRON SIGNAL's collapse as that
bug. It was not. The Anchor Officer trains Tradecraft at 40%, which beats the team's
best Navigate of 34, so the fix changes nothing for this team and the case would have
gone the same way. The real cause is tuning, and it is worth stating plainly:
phase chance across the five-phase ramp (difficult, 0 to −20)
lead 40% 20% → 15% → 10% → 5% → 1%
lead 55% 27% → 22% → 17% → 12% → 7%
lead 70% 35% → 30% → 25% → 20% → 15%
lead 85% 42% → 37% → 32% → 27% → 22%
Tradecraft is a SUPPORT skill on most postings that have it at all, so a realistic
lead is 40–55%, and the back half of every case sits in single figures. Two cases in
a row have now been decided by Resources running out rather than by anything the
players did. This is a design decision rather than a defect — a case is meant to be
survived rather than won — but if phases are supposed to be passable, the ramp
(−5 per phase, on top of `difficult`) is the dial, not `DEFENCE_STEP` and not the
skill ratings.
## The case ramp, retuned
The old ramp made every phase after the first Difficult — which HALVES the lead's
skill — and then stacked a cumulative −5% on top of that. The two pressures were
never the same idea, and they are now separated in `phaseRampFor(index, kindId)` in
`rules.mjs`, with six spot-checks:
- **Difficulty is about the phase.** Only Containment is Difficult — the step the case
is actually about — and it does not also take the ramp. It is hard because it is
containment, not because you are tired.
- **The modifier is attrition.** −5 per phase, capped at −15, because a fifth phase is
not four times worse than a second.
```
was now
lead 40% 40 → 15 → 10 → 5 → 1 40 → 35 → 30 → 20 → 25
lead 55% 55 → 22 → 17 → 12 → 7 55 → 50 → 45 → 27 → 40
lead 70% 70 → 30 → 25 → 20 → 15 70 → 65 → 60 → 35 → 55
```
Simulated over 20,000 full five-phase cases per row, WITH the destitution rule
modelled (a spent track upgrades every remaining phase to Difficult):
```
lead 40% passed 0.72 → 1.37 of 5 hit destitution 87% → 63% of cases
lead 55% passed 1.14 → 2.06 of 5 hit destitution 72% → 40%
lead 70% passed 1.60 → 2.78 of 5 hit destitution 54% → 20%
```
Destitution almost always lands on the final phase rather than compounding, so it
stings rather than spirals — my first description of it as a death spiral was
overstated.
Played two cases on the new numbers. SHORT ORCHARD passed 2 of 5 and ended scraped
clean at Resources 0 but Containment 7. PALE VERGE passed 4 of 5 including two
specials, failed the Containment step, and came home at Containment 9, Resources 4.
Both read as cases rather than as arithmetic.
**R-222 · leading a phase earned no experience.** `resolveLegDialog` called
`skillRoll` with an actor and a rating but **no skill family**, so the choke point
could not resolve the skill item and the tick never fired. This is the same fault the
internal-callers fix addressed — that fix resolves the item FROM the family, and this
caller supplied none — so Tradecraft, the skill the job most uses, was still the one
that could never improve. Fixed and verified: PALE VERGE's lead ticked Tradecraft and
took it 62% → 66% at close.
## SHORT ORCHARD, on the retuned ramp
Containment, Physician, Materials, Pursuit. The case: a person at a farm in Angus is
in two places and both are attending work. The truth is that there is no original
left — both are copies, and one of them knows — and the case is deciding which one
goes into custody. The opposition is another desk of the same agency, tier dangerous,
whose stated leverage is Clearance.
The ramp read as intended: 41% → 36% → 31% → (Containment, Difficult) → and the team
passed the first two comfortably before fumbling the police cordon, which cost two
Containment and a Clearance. That fumble is what brought the rival desk.
They tried the leverage first, which is the right play — but the Containment Officer
fronted it on Status 15% with the Clearance bonus already spent by the fumble, and
failed. Eleven rounds on a farm road: two agents down, two Cleaners down, and the
Materials Officer — the worst shot present at 27% — ended it with a critical to the
head. Four blows were graded during the fight, including a Cleaner's critical cut to
a special by Kester's successful dodge, which still did 9 and a Major Wound. That is
exactly what grading was built to do.
The Physician then patched her own team, in the order she was taught.
**The wound pipeline reaches the case phases**, which is the observation worth
keeping: the last two phases resolved at 11% and 1%, and the chat card says why —
`Wounded: −20%`. The team failed those phases because they had been shot, not because
the ramp is broken. On a healthy team the same phases had been running at 31% and
40%. A GM reading those cards can see the cause, which is the whole point of putting
the modifier on the card.
Case closed at Containment 6, Resources 3, Clearance 5 — spent, but nothing like the
floor the old ramp produced.
**R-222 confirmed across the board.** Every agent came out of this case with ticks —
Dodge, First Aid, Firearm (Pistol), Firearm (Rifle), Navigate — where before the fix
a case produced none at all from phase work.
## Zero hit points now means something
**R-223 · nothing happened at zero.** An agent reduced to 0 hit points was marked down
and then lay there indefinitely. Bleeding deliberately stops at zero, no rule advanced
anything, and the only mention of death in the entire system was flavour text on a
destroyed head location. A game with hit locations, Major Wounds and bleeding had no
way for anyone to die.
Hit points are not taken below zero — every damage path clamps there and unpicking
that would touch everything. Zero starts a clock instead:
above 2 up
2 or below DOWN — out of the fight, still conscious, still yours
0 DYING — unconscious, no actions, no defences, the count begins
count met DEAD
The count is the agent's own **Major Wound threshold**, so the number that already
decides how much damage they take standing up also decides how long they have on the
floor, with a floor of two rounds. It ticks from the same end-of-round hook that
charges blood loss, so nobody has to remember it — which is exactly why bleeding
worked and dying did not exist.
**Stabilising** takes First Aid or Medicine and stops the count. It does not put
anyone back on their feet: still zero, still unconscious, until somebody heals them
above it, which clears the count automatically. A FUMBLED attempt over a dying agent
costs them a round they do not have. `heal()` now refuses the dead, which is the one
place that had to say so because every medical path routes through it.
**The last entry.** A Custodian's death is a filing question. How well filed they were
decides what the record says: at Coherence 7+ a death in service with a form and a
pension; at 4–6 two dates of death on file and Registry asking which is correct; at
1–3 a file that will not close, with correspondence still arriving and being answered;
at 0 they are not recorded as dead at all but filed as transferred, to a section the
department cannot produce an address for.
Four rules, ten spot-checks, on the sheet as a banner in all three states, and
documented in the rulebook. 44 rules, 148 formulas.
## The case generator, simplified and made to say the useful things
**Four phases, not five.** The arc drew TWO interchangeable middle phases, which read
as padding — two seeds in a row produced "The search" then "Inside" with nothing to
tell them apart. One middle phase: Approach, the thing in the way, the job, and out.
**The brief now answers the operational questions first.** It opened on a
classification note and never stated where the site was, how long the team had, what
was likely to be in the way, or how they were getting out — all of which the generator
already knew and threw away. `place` and `duration` were generated and never printed.
The brief now leads with a table of Where / When / Classification / Phases, then the
job, then what is reported, then a full section on the opposition — numbers, tier,
motive, tactics, and what might work instead of shooting — then **How you get back**:
the extraction ground, the Transposition roll, the classification modifier, and the
warning that a bad crossing is the only way an Art is ever acquired.
The same four facts are now on the CASE FILE ITSELF, so a GM does not have to open the
journal to answer "where are we and how long have we got".
Checked across 300 seeds for `[object Object]`, `undefined` and `NaN` in every
generated page: clean. One real leak found and fixed on the way — the group's taboo
and desire are objects with a `.text`, and the brief was printing the object.
## LOW VERGE — played until it killed somebody
Field Lead, Physician, Recording Officer, Translation Officer at a boarding school out
of term. The brief did its new job: Where, When, the job, what to expect, and the way
back, all before the prose. Four phases.
The rival desk never became a fight — the Field Lead rolled a natural 1 on Status 15%
and they stood down, which is the leverage the Opposition page names. The danger was
never them. The Pressure page had already declared the price: *"There is a body that
has to be carried, and it is one of ours."*
Two phases failed, the Containment phase failed, and the other side's weather came
through a gap at the true scale. Three of the four went to zero in a single exposure
and the Physician — 48% First Aid, on four hit points herself — stabilised all three,
one after another, and then did it again after the next wave.
Then the extraction failed on a tidal causeway with the window already past. Marek
Achterberg and Tomas Vance died in the water, both at Coherence 10, both **recorded
properly — a death in service, a form, a pension, a name that stays where it was put.**
Nkechi Quist came home stable at zero. Ambaro Vance walked off the causeway on four
hit points having kept three people alive for six rounds and lost two of them anyway.
**R-224 · a body at zero absorbed damage for free.** Found by this play-through and it
is what killed them. Hit points clamp at zero, so further damage to somebody already
there changed nothing — and a STABILISED agent absorbed it forever, because the count
was held and there was nothing else left to move. You could empty a magazine into
someone bleeding out on the floor and the system would not notice. Damage at zero now
breaks the stabilisation and costs a round of the count, and `hitLocationRoll` calls
`died()` directly when that meets the limit. That is precisely how the causeway killed
two people who were medically stable four minutes earlier.
**Worth noting for the Last Entry table:** both died at Coherence 10 and so got the
dullest of the four entries. Coherence is normally spent on the way home, and these
two never got there — an agent who dies mid-case is likely to be *well* filed. The
interesting entries belong to people who die after a bad crossing, which is the right
way round but means the table's bottom half will be rarer than its top.
## IRON MARGIN — nobody died, and that is the more useful result
Containment, Overwatch, Access, Stores at a market town with a working parish office.
The job as briefed: *make an arrangement, file it, and never speak of the terms again.*
Nobody drew a weapon in the entire case. The Approach came up SPECIAL, the cordon
FUMBLED (−2 Containment, −1 Clearance), Containment succeeded, and the gap surfaced —
the first helpful local had been steering them all day. Then the Stores Officer opened
on the quiet neighbours: special, fumble, success, standing 0 → 2, and there was an
arrangement to be had. That IS the win condition on this case.
Extraction failed on the causeway, which cost tracks and nothing else. Everyone
crossed home. **No casualties at all — not a single hit point lost across four
phases.** Final tracks Containment 7, Resources 4, Clearance 5.
The interesting cost was the crossing. Anneke Oyelaran-Vost fumbled Transposition on
99, took 3 Coherence, and came back with **Witness** at 28% — *"A return that went
wrong. You come back. You do not come back the same."* Which is the rule working
exactly as designed: an Art has no other way in.
Taken with LOW VERGE, the two play-throughs bracket the system honestly. That case
killed two agents because a Containment failure put the whole team at zero in lethal
conditions and the extraction then failed on a tidal causeway. This one closed
socially, in four phases, and cost one agent three Coherence and a power she did not
ask for. The death rules are not a meat grinder; they bite when the fiction has
already gone badly, which is where they should.
## The extraction phase, tuned
Two play-throughs in a row were decided at the extraction, so I went looking. What I
found first was a correction to my own play: **a FAILED extraction has always read
"Phase complete, but at an extra Resources and half again the time."** The team gets
out. Only a fumble fails to complete. In LOW VERGE I treated a failure as *stranded on
the causeway* and invented a tide that killed two agents. That was my adjudication,
not the system, and I was wrong about the rule I was adjudicating.
But the reason I improvised is a real defect.
**R-225 · the window clock was generated and read by nothing.** Every case carries a
clock, and one of the five is *"The window closes"*, whose stated cost is **the way
home** and whose three steps are "the margin narrows", "the return is Difficult", "the
return is aimed at −30%". No code has ever touched it. So the phase whose entire job
is *getting out with it* had no consequence of its own, and whoever ran it had to
invent one — and improvising at that moment is exactly how a phase that should cost
you the trip home ends up costing lives.
The extraction now spends that clock instead of hurting anybody:
critical out clean, and the window opens back up (clock −1)
special out clean (clock 0)
success out (clock 0)
failure out, but not cleanly (clock +1)
fumble you do not get out this attempt (clock +1)
0 steps → 0%
1 step → −10% on every Transposition
2 steps → −20%, and the return is Difficult
3 steps → −30%, and the return is Difficult
`returnCrossing()` reads it alongside the classification modifier and the Coherence
band penalty it already applied, so the cost lands where this setting says the cost of
a case belongs — on the crossing home — and the players can see it coming, printed on
the phase card the moment it happens.
Three rules, ten spot-checks. 47 rules, 158 formulas.
**Also fixed while in there:** the leg carried no `kind`, so the extraction check
matched on the terrain PROSE and worked only because the string happens to begin
"Extraction —". Legs now store their kind, with the prose match kept as a fallback for
case files generated before today.
Verified in play on PLAIN HARBOUR: extraction failed at 47%, the card read *"You are
out, but not cleanly. The window has narrowed... The way home: −10% on every
Transposition roll"*, the clock went to 1, and all four crossings took −10%. Nobody
lost a hit point.
## A briefing, and a millennium of ironmongery
**The players now get a briefing, not a memo.** The generator was already writing the
timeline, the cast and two lines of texture that are the best prose in the whole
document — and filing every word of it behind the GM screen. A new PLAYERS page:
- a read-aloud opening built from the classification, the place and the window;
- **what it is like there** — the texture, with the note that none of it is in the
file, because the file was written by somebody who was not there;
- **what is known so far** — the timeline as a table;
- **who is there** — the cast by name, role, and what they want from you, but never
what they lie about, which stays on the GM page where it belongs;
- and a short "before you go" that tells a new player the three things this game
expects of them: sign for your kit, bring somebody who can make an arrangement,
keep your anchor on you.
Ownership is now **declared by the page** rather than guessed from its index — the
old `i === 0 ? 2 : 0` would have silently hidden any second player page. Two player
pages, four GM pages, 300 seeds checked clean.
Two bugs in my own prose, caught by reading the output: `classification.texture` is a
LIST and was being printed comma-joined into a sentence, and the "since then..."
line was reaching for the last timeline entry, which is *the team crosses* — so every
briefing ended "since then it has not stopped, and the team crosses."
**The historical range.** The department has been running since before it had a name
and has never thrown anything away.
- **38 historical weapons** (from 6): arming sword, longsword, falchion, war hammer,
mace, military flail, poleaxe, halberd, billhook, pike, quarterstaff, dirk, longbow,
arbalest, sling, javelin, rapier, main gauche, matchlock, wheellock, blunderbuss,
boarding pike, cavalry sabre, smallsword, cutlass, Brown Bess, Baker rifle, socket
bayonet, Martini-Henry, Webley, Lee-Enfield, trench club — each with its period
stated, because a GM handing out a matchlock should know it is a matchlock.
- **14 historical armours** (from 2): gambeson, maille shirt, coat of plates, cuirass,
full harness, buff coat, sallet and bevor, lobster-tail helmet, jack of plate,
cuirassier armour, Brodie helmet, flak jacket. Weight is the point — full harness is
9 AP and 25 kg, and the free-carry rule does the rest.
- **Five new skills** to hang it on: Melee Weapon (Polearm), Melee Weapon (Flail),
Missile Weapon (Sling), Missile Weapon (Thrown), and **Firearm (Muzzle-loader)** —
because a muzzle-loader is not a rifle and loading it is the skill.
- **275 icons**, all generated; every weapon maps to a real skill and every item has
art, both checked.
Catalogue is now **206 items**: 118 gear, 62 weapons, 25 armour, 14 vehicles — 145
modern, 77 antique, 34 future, 19 anomalous.
## GLASS LANTERN — the historical gear in play
A village with two war memorials, Hollow class, salvage era **antique**. Antiquities,
Containment, Rigging, Physician.
Salvage from a Hollow world did exactly what the era tag promises: a **poleaxe, a
chirurgeon's roll, a lodestone and a pocket watch**. They armed themselves out of the
village — poleaxe to the rigger, a Brown Bess and a maille shirt to the Antiquities
Officer, a halberd to Containment, a sling to the Physician.
Three things the play-through proved, all of them the right behaviour:
**Salvaged historical weapons are not automatically usable.** The poleaxe, halberd and
sling all came up at **5%**, because Polearm and Sling are skills nobody on the team
has. The Rigging Officer knows her boarding axe at 68% and a poleaxe at 5% — the same
shape of weapon, a different skill, and she sensibly kept the axe. Salvage is a
promise of usefulness, not a free upgrade.
**Historical armour makes a knife pointless.** The Antiquities Officer in maille over
brigandine came out at AR 8, and the village hit her four times with utility knives
for **0, 0, 0 and 0 damage**. 1d4+2 against eight points of armour is nothing, and
that is exactly the historical fact the numbers should produce. She was also
**overloaded at 28 kg against a 20 kg free carry**, which is the other half of the
same fact.
**Muzzle-loaders behave.** Flintlock and Brown Bess both hold one shot with a
two-round reload. She fired the flintlock in round one, and thereafter fought with the
duelling sword rather than spend two rounds reloading in a melee — which is the
decision the rule is supposed to produce.
The fight ran seven rounds. The village lost one dead and two dying; the team took
almost nothing, because they were wearing four hundred years of armour development and
the village had knives.
**R-226 · "Tradecraft first" was too literal.** My own R-221 fix overshot. A
Containment Officer whose Tradecraft had caught a stray bonus point sat at 12% against
a printed base of 5 — technically trained — and led every phase ahead of a Rigging
Officer on **53% Navigate**. The preference is meant to mean "whoever reads a
developing situation best", not "anybody with a point in the right column". Tradecraft
still wins ties and wins when it is genuinely better; it no longer wins when it is
plainly worse. The Containment phase went from 6% to 26% on the same team.
## An overnight build: hazards, automatic fire, two weapons, a bestiary, and space
### Stress test first
150 characters — every one of the 25 postings, at Probationary and Case Officer, in
all three species — checked against fourteen invariants each. Nothing broken. Then the
edges: characteristics at 1, 3 and 21; every location destroyed at once; twelve pikes
carried; hit points set to −99. All degraded gracefully except one.
**R-227 · a destroyed head was a nap.** The printed effect has always read "For
anything that keeps its brain there, this is death" and the code set `unconscious` and
stopped. An agent with every location on their body destroyed, head included, reported
as DOWN. `destructionOutcomeFor()` now says what destruction means: a head is death, a
vital or sensory location drops you to zero, a limb is ruinous and survivable.
### The ways the world kills you
- **Falling** — a die per 3 m, random location, **armour does not apply** because the
ground is not a weapon. Soft landing halves the distance; a controlled descent
halves it again. Verified: 12 m killed an agent outright; the same fall controlled
left them on 2 hit points.
- **Drowning** — CON rounds with warning, half without, then a point a round that does
not stop. Runs straight past the Major Wound threshold to zero and into the dying
count. Verified over 24 rounds.
- **Fire** — a condition with a size, not a hit. Worn armour helps on the round you
CATCH and never after, because by then the fire is inside it.
- **Explosives** — one roll at the centre, shared by band, and **cover beats armour**:
armour counts at contact only and only half. Verified on four people at 1 m, 4 m,
10 m and 40 m: 17, 3, 0, and outside it entirely.
### Automatic fire, and two weapons
One roll for a burst, and the **success level** says how much of it arrived — the same
band machinery as everything else rather than four rolls and a stalled table. **The
rounds are spent either way.** Verified: a failed full-auto burst spent ten rounds for
nothing, which is the honest part of the rule.
Two weapons costs −20/−40 untrained and 0/−20 with the new **Ambidextrous** talent.
Nothing removes the off-hand penalty, and the off hand takes the lighter weapon at no
more than 2 kg — the rule that refuses two poleaxes, tested and refused.
### The bestiary
Twenty-one entries in three families, each with characteristics, natural armour, a
tactics line and a POWER written as a rule with a cost rather than a special ability.
FOLKLORE: barghest, kelpie, redcap, boggart, church grim. HORROR: hollow man, the
tenant, the choir, the carrion file, the understudy. FORWARD: surveyor, quarantine
unit, cuckoo array, long walker, the auditor. Every skill and weapon reference checked
against the catalogue.
### Space, and everybody in it
Eight STATIONS, each using a skill somebody already has, so the Physician and the
Translation Officer have a chair. Range is a band; the helm moves one a round, in or
out. **Heat is the pressure** and it is the whole design.
Two rounds of tuning, both from playtest and both necessary:
- First cut had every station heating and one engineer venting. Heat reached the
ceiling by round four and locked the helm — a countdown, not a decision. Now only
Gunnery is charged for trying (the shot left either way, exactly as the burst rule
reasons), looking is free, and venting sheds 3.
- Second cut was too cold — heat sat at 0 and nobody cared. Settled between.
- **The helm only ever closed.** Every fight ended docked with no way to break off
short of the anchor. The helm now chooses in or out.
Final playtest: heat 2 → 5 → 3 → 5 → 7 → 10, and at the ceiling the helm locked out
because the crew kept shooting instead of cooling. That is the decision the subsystem
is for.
64 rules, 198 spot-checked formulas, a 12-page rulebook in reading order.
### The Codex pass, and what it cost me
Put the night's five subsystems to Codex with the instruction to be blunt. It found
three things that were genuinely wrong and I have fixed all three.
**R-228 · Ambidextrous was a compulsory build.** At 0/−20 the talent gave a
full-strength main hand AND a cheap second attack — near-free action economy, and
therefore the only sane pick for anybody who could hold two things. The 2 kg limit
stops poleaxes, not optimisation with a pistol and a knife. Ambidextrous now buys
CONSISTENCY rather than a free attack: **−20 in both hands**, against −20/−40
untrained. Still plainly worth having; no longer mandatory.
**R-229 · full auto was a firing squad.** Ten independent hits off one critical, each
with its own damage roll, hit location and Major Wound check. Against a hard target it
was the correct answer to everything. Hits are now **capped at 3/5/7** rather than
proportional to the burst, and however many rounds connect, **a burst is one wounding
event** and rolls the Major Wound table at most once.
**R-230 · Engines deleted the pressure it was supposed to create.** Guns +2 and Helm
+1 against a vent of 3 made a standard fighting round heat-NEUTRAL — the craft cooled
while it fought, and Heat only existed when the engineer failed or was absent. This
is the same fault as my earlier over-correction, in the other direction, and my own
second playtest had shown it sitting at 0 for four rounds without my drawing the
conclusion. Venting is now 2/3/4, so a shooting round costs +1 net.
Two more taken as written:
- **Falling halving stacked.** Soft ground and a controlled descent both halved, so
together they quartered — the same mitigation counted twice. They no longer stack.
- **Blast ordering was undefined.** Flooring at each step meant the answer depended on
the order the functions were called in. One formula now, floored once: rolled x
range share x cover, rounded down, and only then armour.
**And the sharpest observation, which I acted on structurally:** "giving everyone a
roll is not the same as giving everyone a decision." Sensors, Damage Control and
Medical were seats that rolled dice while the gunner decided the fight. Each now has
mutually exclusive modes — Sensors marks, spoofs or reads; Damage Control patches or
BRACES so the next hit lands at half; Medical restores a crewman or STEADIES the ship
so the heat penalty does not apply that round. Bracing and steadying both bite in the
engine, not just on the card.
Not taken tonight, recorded as owed: station fumble tables, Signature as a live
mechanic, mark duration and stacking, hull and system criticals, extinguishing fire,
rescue from drowning, and an explicit interaction table for hazards against armour,
Major Wounds, bleeding, zero and dying. Codex is right that these are implied; they
are a session's work, not a paragraph's.
## SALT COUNTY — the bestiary in play
A county records office over a bank, and a member of the public behaving as somebody
who died eleven days ago. Field Lead, Physician, Night Warden, Antiquities Officer.
The generator set this up better than I could have. The brief names a **revenant** —
which is in the bestiary already — and the truth page says the cause is *elsewhere in
the same location and has not been noticed*. In a records office, that is the
**carrion file**.
**The revenant was resolved without a shot.** Its printed leverage is "tell it where
that person is, and it will go, and that becomes somebody's problem" — the Antiquities
Officer made the Knowledge roll off the parish register and it left. That is a
statblock's tactics line doing the work a fight would otherwise have done.
**Then the gap surfaced, and the file did what it is for.** AMENDMENT is written as a
rule with no saving roll, and that is deliberate: it removed the Antiquities Officer's
Language (Trade Cant) at 63%, the Night Warden's at 79%, and the Field Lead's Dead
Reckoning talent — permanently, with no roll offered and nothing to roll against. The
horror of that entry is entirely in the absence of a defence, and it reads correctly
at the table.
**Burning it exercised the new fire rules on a thing rather than a person.** Armour 6
counted on the round it caught and never again: 3, then 6, then 11, and the file was
at zero in three rounds. The team were standing in the same burning archive and took
the "clothes alight" band — the Antiquities Officer's armour 4 absorbed it completely,
the Night Warden's 2 did not.
The case closed at Containment 8, Resources 1, Clearance 5. A failed extraction
advanced the window clock to 1, and every crossing home took the resulting −10%, which
is the retuned extraction rule working end to end.
**One honest note on my own reporting:** a probe line read "Dead Reckoning: gone" for
all four agents. Only the Field Lead ever had it — the other three had never been
awarded it, and the probe could not tell the difference between a talent removed and a
talent never held. The amendments actually made were three, not four.
## Fire modes, fitted kit, and a rail instead of a strip
Four requests, and the first of them turned over the usual stone.
**R-231. Every weapon has carried a fire mode since the packs were first built, and
nothing has ever read it.** A musket could rip a ten-round burst; "rapid" bought a
weapon precisely nothing. `FIRE_MODE` now gates the burst rules and carries its own
attack modifier, so the mode is a decision rather than a label: **slow** +10% and then
you work for the next one, **single** the baseline, **fast** a second shot at −20% to
*both*, **rapid** bursts, **beam** +5% and it can be held. The gate is checked inside
`burstAttack` rather than only in the dialog, so no macro or harness can route round it.
**R-232. `fast` was not a fire mode at all.** Five weapons carried it — longbow, sling,
javelin, the Webley and the Lee-Enfield — and the system's list of modes did not
include it, so the item sheet's dropdown had no matching option and re-saving one of
those weapons silently blanked its mode. The list was hand-written next to the rule
instead of derived from it. It is derived now, and the guard that should have caught
this was the one I had to fix in R-236.
**R-233. `burstAttack` was reachable from nothing.** The automatic-fire rules shipped
complete and could not be used from any sheet. It is now the resolution path the fire
selector chooses, and it takes the dialog's whole modifier stack rather than rebuilding
its own — the first version charged the player twice for the same burst.
**R-234. Any weapon with a Coherence cost could not be attacked with at all.** The
handler read the weapon, awaited the Coherence charge, and then read the weapon
*again* — but `currentTarget` is only set while an event is being dispatched, so the
second read returned null and the attack died on a TypeError with nothing shown to the
player. The Redaction lance, the Unmaker and every piece of transposed materiel were
unusable, and the failure was completely silent. Resolve once, before anything is
awaited.
**R-235. The damage card was the last one wearing Foundry's stock dice styling.** It
posted a bare Roll and let the core template dress it — grey boxes on a white panel in
the middle of a navy sheet. It renders through the module's own card now, with the Roll
still riding on the message so the dice stay inspectable and Dice So Nice still
animates.
**R-236. The duplicate-rule guard reported an honest table as a copy.** `FIRE_MODE` has
`slow` and `fast`; so does the entirely unrelated `speedBands` vocabulary, and two
shared key names was enough to fail the build. Every real instance this guard has ever
caught duplicated the *whole* table, so it now wants a majority of the keys — and
prints partial overlaps rather than swallowing them.
**Sights and lamps.** A sight is not a flat bonus, which is why most systems end up
with everybody carrying the same scope. It is graded by range band, so **a telescopic
sight is a −10% liability inside ten metres** and worth +20% at extreme, and it only
counts if it *fits* — the scope and the thermal sight are rifle furniture. Because the
band is already known by the time the modifier is wanted, the sight applies itself:
there is nothing to select and nothing to forget. The dark now costs something, a
carried lamp buys it back to dim and never to daylight, and the card says plainly that
the lamp shows them exactly where you are.
**R-237. "Automatic" that needs a re-import is not automatic.** Agents created before
those fields existed carried a Field lamp whose `lightSource` was the template default
of false — the lamp sat in the pack doing nothing. A migration stamps fitted gear from
the compendium on load, matched on `flags.ringbrp.key` rather than on display name,
because the fuzzy-name match this project used before once mapped a climbing kit to
breaching charges.
**My own error, caught by playing it.** I first costed a sustained beam as
`multiplier × energyPerShot`, which made a locked beam cost 60 charge from a 20-charge
cell — unfirable, and the failure was a warning the player would read as a bug. The
magazine is spent one per shot everywhere else in the system, so the cost is in shots:
a locked beam burns six. Worth noting that `energyPerShot` itself is written by the
pack builder and **read by nothing** — the same defect again, left alone here because
wiring it would change every energy weapon's economy.
**The tabs.** Eight of them across the top of an 820px sheet compressed to 11px type
and were genuinely hard to read. They are a rail down the side now: one line each,
larger type, a solid block for the active tab, and counts as badges. The sheet is 152px
wider to pay for the rail so nothing else got tighter, and the skills list is driven by
the space actually available rather than by a window-width breakpoint that was
measuring the wrong box.
Also noted, not fixed: fourteen rules are imported into `ringbrp.mjs` and read nowhere
in it. They are live rules with real readers in the rulebook generator and the guards,
so this is dead *imports* rather than dead rules — but it is the same smell.
## THE GOODS YARD — the four fire modes, side by side
One agent per mode, all four trained to exactly 60%, so the fight reads the mode and
not the shooter. A barghest in a coal yard at 02:40, pitch dark, one lamp between
four of them. Four rounds. The dog left at 4 hit points of 16 and nobody caught it.
**The modes separated cleanly, and not in the way I expected.**
**Slow was the best weapon in the fight.** Dunn's Baker rifle hit on both the shots it
got, and the +10% is what bought them: round one at 35% he rolled 33, and at the 25%
he would have had without it that is a miss. The reload is a real cost — he was out of
the fight for rounds two and three and the dog reached him — but a mode that turns a
33 into a hit twice is not the weak option.
**Fast is not two chances to hit. It is a second defence at a penalty.** Rakhi's two
Webley shots in round two both landed at 20%, and the first was dodged clean. The
second one hurt precisely because the barghest had already spent its free defence and
had to dodge again at −30%. That is the mode's actual function and it is better than
the tooltip suggests.
**Rapid was the worst thing in the yard.** Ceyla fired 20 of 30 rounds across four
rounds of combat and hit nothing at all. A full magazine at −30%, in the dark at −30%,
is a 1% roll that still spends ten rounds — and the rules charge her honestly for
every one. This is not obviously wrong; automatic fire into a dark coal yard at
seventy metres *should* be futile. But it means **the burst options are close to
unselectable in any fight where the light is bad**, and the light is bad in most of
this game. Flagged rather than tuned: I would rather see it in a second fight before
touching the numbers.
**Beam did the damage.** The locked beam landed for 10 — 1d4+6 with its 2d6 riding on
the standing blow exactly as designed — and took the barghest from 11 to 4 through
armour 3. It cost 6 charge a shot and, because the lance is transposed materiel, **1
Coherence every time Tomas pulled the trigger**. He ended the fight Loose at 6, with 2
charge left. That is the most interesting economy in the fight and nothing needed
changing to produce it.
**The sight graded itself the way it was meant to.** Ceyla's telescopic sight was +5%
at seventy metres and **−10% once the thing was inside twenty-five**, and she felt it
both times. Nobody had to remember it.
**The lamp is the finding.** Tomas's Field lamp turned pitch dark into dim for him — a
40-point swing, the largest single modifier in the fight — and the card told him each
time that it showed the barghest exactly where he was. Which it did: the dog went for
the lamp in round two. But **a lamp only lights its carrier**. Three agents stood
inside the same pool of light at −50% while the fourth shot at −10%. That is wrong at
the table, and it is the one design change this fight actually argues for.
### Defects the fight surfaced
**R-238. A fast action fired twice and paid for one round.** The Webley put two shots
out and the cylinder went down by one. Ammunition was charged once before the shot
loop instead of once per shot — the exact dishonesty the burst rules were written to
avoid, and the mode that ought to cost the most was the only one getting a free shot.
Charged per shot now, and the loop stops when the weapon runs dry mid-action.
**R-239. `morale.downThreshold` is written by the pack builder and the template and
read by nothing.** Every NPC in the game carries a morale field and there is no morale
rule. Whether the barghest broke off was mine to decide with no support from the
system, which is fine as a GM call and not fine as a shipped field. Not fixed — it
needs a rule, not a patch.
**Two of my own, worth recording because the pattern keeps repeating.** I did
arithmetic on `system.coherence` (an object) instead of `.value` and reported a
Coherence loss that never happened; and I read `system.hitPoints`, which does not
exist — the field is `system.hp`. Both were caught inside a minute by printing the
result instead of trusting it, which is the only reason this log is accurate.
**A content wart:** the barghest bites with something called "Vesh grasp limb" — the
generic `grasp` weapon still carrying a Ringworld-era display name. Every creature in
the bestiary that uses `grasp` inherits it.
## R-240. A lamp lights the people standing in it
The goods yard fight made the case: three agents inside the same pool of light shot at
−50% while the fourth, four metres away, shot at −10%. Nobody would run that at a
table, and the rule was wrong rather than merely coarse.
A lamp now has a **reach**, and it is per item rather than global, because a weapon
lamp and a vehicle searchlight are not the same offer — 6 m, 10 m for the issue field
lamp, 4 m for cold light strips, 30 m for a roof searchlight. Anyone inside the reach
is in the light, and the card names whose lamp it is: *"MODE: Tomas's, 4 m away — you
are in it."* Both halves of the rule travel together, so the pool is one position and
the whole group is standing in it.
Measured from the tokens, because the canvas already knows who is standing where. The
same shot that started this — Rakhi, no lamp of her own, four metres from Tomas's, in
pitch dark — went from **20% to 60%**, and Ceyla twenty-five metres out still ate the
full −50%.
**The interesting part was the unknown.** `Number(null)` is 0, so an unmeasured
distance read as *standing on top of the lamp* and would have lit any scene with no
tokens placed. My own spot-check caught it before it ever ran: an unknown distance is
now explicitly not lit, and the dialog carries a tick that arrives pre-set when the
canvas could measure it and is the GM's answer when there is nothing to measure. That
keeps it automatic where it can be and honest where it cannot, rather than quietly
lighting the world.
Note for deployment: `template.json` is read when Foundry loads the system, so the new
`lightReachM` field did not exist on items until the server was restarted — the
migration then stamped it. A remote server needs the restart, not just a pull.
## The combat tab, reordered — and R-241, a weapon nobody could ready
**R-241. There was no way to ready a weapon.** The strip under the characteristics
reads `equipped`, and the only `toggle-equipped` control on the whole character sheet
was on the *armour* list. A weapon could only be readied by opening its item sheet and
finding the tickbox there — which is why the strip on most sheets showed whatever the
pack builder happened to have equipped and nothing else. The weapons table now carries
the same toggle, the row is tinted when it is readied, and a line above the table says
plainly what the control does.
**R-242. The off state did not look off.** `.ringbrp-sheet a` is more specific than a
bare `.rb-equip`, so an unequipped toggle was painted white and read as lit. That was
true of the armour toggle too, and had been since the toggle was added.
**The tab now runs in the order it is used**: weapons, armour, defend, reaction, and
the body last. Weapons lead with 40px art and a display-face name, because they are
what the tab is for and they were previously below two screens of anatomy and reaction
arithmetic.
**The figure is a partial now.** It is drawn in three places — the header thumbnail,
the combat tab and the anatomy tab — and it was previously twenty lines of inline
Handlebars that existed once. Three copies of that would have been the template-layer
version of the defect `check-rules` exists to prevent: a location present in one
drawing and missing from another. One partial, registered at init, three call sites.
The header now carries a **34px figure and every location with its damage and the
armour over it**, so the commonest question at the table — how much armour is on the
part that just got hit — is answered on the main page without changing tab.
Noted while working: the mini figure inherited `.rb-body-svg { max-width: 180px }`
from further down the stylesheet and blew the header out to 385px tall. Same-specificity
selectors, later one wins; the compound `.rb-body-svg.rb-body-mini` fixes it. Worth
remembering that this stylesheet is ordered by section rather than by specificity.
## R-243. A helmet was protecting your legs
Asking for "armour and the type of armour" on each location turned out not to be a
display request. Every equipped piece added its points to **every** location, so a
Brodie helmet protected your shins and a cuirass protected your skull. With a
catalogue that contains sallets, lobster-tail helmets and three separate cuirasses
that is not a simplification, it is wrong — and it could not be printed on the sheet
without the sheet saying something visibly false.
Armour now declares **coverage**, as a named set rather than a list of locations,
because a piece has to fit whatever body is wearing it: `all`, `torso`, `torsoArms`,
`torsoLimbs`, `head`. The sets expand to location *kinds*, so a cuirass covers the
torso of a baseline agent and the trunk and ridge of a vesh without either the item or
the wearer knowing the other exists. Unstated coverage is `all`, so anything that does
not declare behaves exactly as everything did before.
Eighteen of the twenty-five catalogue pieces are not whole-body and now say so.
**The trap was double counting.** `hitLocationRoll` subtracted `sys.armourPoints +
loc.armour` — the global worn total plus the body's own armour at that location.
That was correct while armour was global and becomes a double count the moment the
location knows its own pieces. It now reads the location's total and nothing else,
checked by firing fourteen hits of 10 damage at every location and asserting the
damage applied equals `10 − that location's armour` each time. All fourteen matched.
**A single ARMOUR number no longer exists**, so the header shows the torso — which is
what the question means, and where most hits land — with `0–8 by location` beside it
when the rest of you is not that. Worked example from the verification: stab vest,
cuirass and a Brodie helmet gives 8 on the torso, 3 on the head and **0 on both arms
and both legs**, where before it was 11 everywhere.
**This is a real difficulty change and it is meant to be.** Agents in partial armour
are markedly more fragile than they were an hour ago — limbs are frequently bare, and
limbs are 12 of 20 on the melee table. Anyone who wants the old behaviour sets every
piece to `all` on its item sheet.
## Coherence, promoted
Moved out of the derived strip and into the vitals at the top right, at the same size
as hit points, with its band under it and the box itself changing colour as it falls —
yellow at Adrift, red at Unmoored and Displaced. It is the number that measures whether
an agent is still filed as themselves, and it was set in 11px grey between MOV and
carried weight.
## THE CHANTRY RUIN — coverage, measured
The point of this fight was to make coverage the only variable. Two officers built
from the same pregen, both 12 hit points, both Sword 55% and Dodge 45%, and armour
chosen so that **the one carrying more points was carrying it in fewer places**:
* **Suited** — Breaching suit. **7 points, on all seven locations.**
* **Plated** — Cuirass and a Brodie helmet. **9 points**, on three.
Against a redcap: Polearm 80%, Dodge 75%, a halberd at 1d10+2, and POWER: NOT TIRED,
which ignores the cumulative defence penalty so every parry it makes is at full skill.
In melee **fourteen of the twenty hit locations are limbs**, which is the whole test.
**Plated's own armour worked perfectly, and it did not matter.**
| Round | Where the halberd landed | Armour there | Rolled | Through |
|---|---|---|---|---|
| 1 | right leg | 0 | 5 | **5** |
| 3 | abdomen | 6 | 6 | 0 |
| 4 | head | 3 | 4 | 1 |
| 5 | right leg | 0 | 10 | **10** |
Four connecting blows. The cuirass and the helmet between them stopped fifteen points
of damage. The two that found a bare leg did every point they rolled, destroyed the
leg outright (12 damage against a location maximum of 6), took Plated to zero and
started a six-round dying clock with a bleeding Major Wound. **Under the old rule that
leg carried 9 points and both blows would have done nothing at all.**
**Suited took the same weapon on a limb and felt nothing.** Round 6, left arm, 3
rolled against 7 — zero through. Round 2 and 8 and 9 turned aside on the dodge. After
nine rounds against an 80% attacker they were still at 12 of 12.
**And then full coverage turned out not to be immunity.** Round 10 the halberd came in
on a **critical**; Suited dodged *successfully* and a success against a critical only
reduces it one step, so it landed as a special — half armour — and 12 damage put 9
through a 7-point suit. Round 11 finished them. Zero to zero in two rounds, with a
catastrophic Major Wound.
**What the fight says about the change.** Coverage is now the single most important
thing about a suit of armour, far more than its points: 9 points in three places lost
a leg and nearly a life, 7 points in seven places went nine rounds untouched. That is
the correct shape and it is a large difficulty swing for anyone in partial armour —
which, in a catalogue with this many cuirasses and helmets, is most people. Critically,
it did not make full armour boring: a critical halves it and a maximum-damage halberd
still gets through.
Saved by the rule that has been quietly working since R-223: First Aid at 37% failed
on the first attempt and landed on the second, staunched the bleeding, healed 3, and
`reviveIfHealed` took Plated out of the dying state without being asked. My harness
then called `tickDying` on a revived character and read `.state` off the null it
correctly returned — the rule was right and the probe was wrong.
## Shields — armour with a switch
A shield is the one piece of armour you *hold*, and it protects you exactly as much as
you are currently holding it up. Everything interesting follows from that switch
having a cost, so the design is: raised it is armour and a defence, lowered it is
weight, and the price of keeping it up is that it occupies a hand and you are shooting
around it.
* **Raised** — its points apply to what it covers, and it is the only armour you can
also defend with, on a new **Shield** skill.
* **Lowered** — nothing. Verified: heater shield slung reads 0 on every location,
raised reads 4 on torso and both arms and 0 on legs and head.
* **The cost** — −20% to your own ranged attacks while it is up, printed on
the card as plainly as anything else: *"Heater shield −20%"*. That is what stops
"always raised" being the only answer; it is obviously right for a containment
officer and obviously wrong for a marksman.
* **Only one goes up.** Raising a second lowers the first. Two shields is not a stance.
Seven of them, from a 2-point buckler to a 6-point ballistic shield, with the pavise
and the barrier plate covering legs as well because they are walls you carry.
**`degradation` finally does something.** It has been on every armour item since the
packs were first built and was read by nothing. Shields wear: a blow costs the shield a
point for every step the attack stood above an ordinary success, so an ordinary hit
costs it nothing, a special costs 1 and a critical costs 2. At its limit it comes apart
— no points, nothing to parry with, and it drops out of your hand and refuses to be
raised again.
### Two defects found by testing it rather than reading it
**R-244. Every shield defence quietly rolled Dodge.** I added `"shield"` as a third
defence type and passed it through `defenceTypeAllowed`, which returned it unchanged —
so `kind` was `"shield"`, neither of the `kind === "parry"` branches matched, and the
function fell through to the dodge branch. It was invisible in the chat card and only
showed up as a statistical smell: eight rolls at a supposed 85% produced six failures.
Taking a blow on the shield **is** a parry — it locks with parries and is not a dodge —
so the shield is a flag on a parry rather than a type of its own.
**R-245. The wear rule was unreachable.** I first wrote it to fire only when the shield
turned a blow aside *completely*. But under graded defences only a critical fully stops
a critical, so a shield would have gone its entire service life without a scratch. Wear
is for what the shield **absorbed**: a shield that drags a critical down to a special
has very much taken that blow, and that is exactly when shields splinter. Ten forced
criticals produced zero wear under the first rule; under the corrected one the shield
went 0 → 2 → 4 → broken in three absorbed blows.
Both were mine, both were introduced in this change, and both were found by driving the
real functions rather than by reading the diff.
## THE BELL TOWER STAIR — shields, and what they cost
Two officers, identical kit — riot shield, stab vest, 12 hit points, Shield 65% — and
one difference: the **Warden** holds the shield up on the stair, the **Marksman** keeps
it slung and shoots. A redcap with an 80% halberd.
**Raised, the shield is most of your armour.** Warden with it up: torso 5, arms 3.
Marksman with it slung: torso 2, arms 0. The same item, the same character.
**An ordinary blow turned aside costs a shield nothing, and that is the right call.**
Rounds 1 to 5 the Warden parried four halberd blows on the shield and it finished
without a mark. Had every parry cost a point, a 5-wear riot shield would have been
kindling before the fight was properly started.
**Round 8 is the rule working.** A **critical** halberd; the shield parry succeeded,
which under graded defences drags a critical down one step rather than stopping it. The
shield **took 2 wear for the blow it absorbed** and the reduced blow still landed — on
the left arm, where the shield's 3 points were, for 7. Warden 8 → 1. That is a shield
doing exactly what shields do: it did not save him, it made the difference between a
wound and a killing.
**Both officers were killed through the gaps.** A riot shield covers torso and arms.
Every wound in the fight landed somewhere it does not reach:
* Warden — right leg 4, left leg 10, and down at zero, dying.
* Marksman — **head, 10 damage against a location maximum of 5.** Destroyed. Dead
outright, not dying, which is R-227's fix holding.
**And the ranged penalty decided the fight.** With the Warden down, the Marksman raised
the shield and kept shooting — at 65% less 20%, so 45%. Four shots: **93, 89, 73, 65.**
That last one is the whole rule in a single number. **65 is a hit at 65% and a miss at
45%.** The redcap was on 3 hit points of 13 and the shield cost the Marksman the kill,
and then the fight, and then his life.
The redcap finished on 3 and walked away.
**What this says about the design.** The trade is real and it is legible at the table:
the shield roughly doubled the torso armour of both agents and turned aside five blows
between them, and it also cost a marksman a creature that was one hit from dead. Wear
is paced correctly — 2 of 5 on the Warden's shield after a critical and four ordinary
parries, so a shield lasts a fight and not a campaign. Nothing needed tuning after the
two defects found while building it.
Worth noting for anyone issuing shields: a riot shield is `torsoArms`, and in melee
**legs and head are 10 of 20**. A shield is not a suit of armour and this fight is what
that sentence costs.
## Riot shields for Containment and the Night Warden
Both postings now draw a **riot shield** from stores, and both are trained to use it.
Issuing a shield without the skill would have been R-212 again with a different item —
the Antiquities Officer who drew a flintlock and rolled 5% with it — so
`melee_weapon:shield` goes into each posting's support list. It is **swapped in, not
added**: keeping five core and five support means the shield arrives without a free
power bump, which is not what was asked for.
* **Containment** drops `stealth`. A posting whose blurb is "goes through the door,
holds the corridor" was always the least stealthy thing in the department.
* **Night Warden** drops `navigate`. They sit with one site all night; they are not
going anywhere.
**check-kits now covers shields.** The guard has always required a posting to be
trained in the weapons it carries, and a shield is armour you defend *with*, so it
needs its skill for exactly the same reason. Verified by removing the skill from the
Warden and watching the build refuse:
check-kits: FAILED
warden (Night Warden): issued Riot shield, trains no melee_weapon:shield
**What it does to the two postings.** Four generated agents, shield skill landing at
25–30% (the support tier), and none of them encumbered — though a Containment officer
came out at 32.9 of 33 kg, which is as close to the line as this kit should get.
| | slung | raised |
|---|---|---|
| Containment (breaching suit, `all` 7) | 7 everywhere | torso and arms **10**, legs and head 7 |
| Night Warden (stab vest, `torso` 2) | torso 2, all else 0 | torso **5**, arms **3**, legs and head 0 |
The shield ships **slung**. Raising it is a choice made at the table, which is the whole
design — and for the Warden it is the difference between two points of armour and a
real one.
## Trades — recruited from an ordinary job
Ten of them, and they are not postings. A posting is what the department made of you;
a **trade** is what you were before any of it, because there is no degree in crossings
and the department has to recruit somebody. So it recruits a doctor, a joiner, a
detective sergeant, and puts them through six weeks at the training centre.
Construction Worker · Doctor · Professor · Engineer · Law Enforcement · IT Specialist ·
Scientist · Linguist · Entertainer · Criminal.
**A trade character is built in two halves, and the join is the whole point.**
* The **trade** is genuine expertise, rated on exactly the same bands a posting's
skills are. A doctor is a proper doctor: Medicine 55, Biology 59.
* **INDUCTION** goes on top at **25–40%** — Tradecraft, Anomaly Lore, First Aid,
Firearm (Pistol), Dodge — plus a sidearm, a vest, cordon kit, a cover identity and a
ward, on top of standing issue.
That band is low deliberately. Six weeks is not a career, and a surgeon recruited last
year is a far better surgeon than they are a Custodian. Ten generated agents show the
gap plainly: trade skills land in the high forties and fifties, induction in the
twenties and thirties, on the same sheet.
**Induction never demotes you.** A former police officer already trains Firearm
(Pistol) and Tradecraft, so induction skips them rather than re-teaching them at a
worse number — the generated Law Enforcement agent carries pistol 58 and tradecraft 58
from the trade, not 26 from the course. They do end up with two sidearms, their own
revolver and the issued pistol, which reads as correct rather than as a bug.
**check-kits now guards trades and induction too.** Same standard as a posting: every
kit key must name a real item, every skill and talent must resolve, no posting may be
issued a weapon it cannot use, and INDUCTION is checked once because a typo in it is a
typo in ten characters at once. Both new paths were verified by breaking them:
criminal (Criminal): issued Marksman rifle, trains no firearm:rifle
induction: issues Compact pistol, teaches no firearm:pistol
## Rolling them yourself
The generator can produce eight characteristics in a millisecond and nobody at a table
has ever wanted that. There is now a tickbox — *Roll the characteristics myself* — that
walks them one at a time, each roll announced to chat so the dice actually fall, with
"Take them" locked until all eight are down and a rolled characteristic standing once
it is rolled.
**A hand-rolled character is taken exactly as it fell.** The automatic path takes the
best of three for the posting's key stat and applies the species shift; the hand-rolled
path does neither, and the code says why. Verified: eight nines passed in came back as
eight nines for both a baseline and a vesh, while the automatic path on the same
posting produced STR 17.
Worth recording as a probe error rather than a defect: my first check compared the two
sets with `JSON.stringify` and reported a mismatch. Every value was identical — the key
order was not. The rule was right and the comparison was wrong.
## PAPER MARGIN — a doctor, eleven months in
A farm at the end of a private road in Angus. A person is in two places and both are
attending work; the town has explained it as twins for eleven days. The team: **Dr
Anwen Pryce**, probationary, recruited from A&E — and two career officers, a
Containment Officer and a Pursuit Officer.
The generator handed this case its own best argument for trades. The truth page reads
*there is no original left; both are copies, and one of them knows*, and it closes by
one of them being taken into custody. **Telling two identical people apart is a medical
question**, and on that team Medicine reads 42, 1 and 1.
**The two halves showed up immediately, in both directions.**
*The doctor was the only one who knew what she was looking at.* Anomaly Lore: Pryce 37,
Kobe 0, Halloran 0. Tradecraft: 31, 5, 5. The career officers train neither, so the
newest person on the team was the only one carrying the department's own vocabulary —
which is induction working exactly as intended and reads as true at a table.
*And she was hopeless at the part that closes cases.* Her Persuade is **23**. Offered
the duplicate the truth, rolled 95, and it read her on 37 against Insight 60. She could
crack the case and could not talk anyone through it.
**The scene the whole design was for.** The physical examination failed by three — of
course it did, they are identical — and the bloods came back **critical on 02**. Both
samples are the same sample. Not twins, not an original and a copy: the same person,
twice. Neither career officer could have found it and neither would have thought to look.
**R-226 fired, correctly and visibly.** Pryce trained Tradecraft 31; Halloran trained
Navigate 34. The phase lead went to Navigate — *Tradecraft first, but not when it is
plainly worse* — and Halloran led all four phases, all four succeeding, with Containment
producing a special on 06.
**And the shield issued two commits ago turned up unprompted.** Kobe is Containment, so
she now draws a riot shield; it was slung, she raised it, and went from 7 everywhere to
10 on torso and arms. The duplicate never touched her. It also never touched anybody —
three shots, 77, 57, 95 — while Kobe's rifle put 5 through its chest and then 12 into
its **bare right leg**, because armour coverage applies to the opposition too.
The costs were paid as written: *meeting it costs 2 Coherence, killing it costs 3.*
Everyone dropped to 8; Kobe, who shot her own duplicate, ended **Loose at 5** and −5%
to Communication.
The parish had wanted a body back for years and now there was one that would bury.
Making it into something a family could be given is a doctor's job, and she rolled
**42 against 42** — a success by nothing at all.
### One probe error, worth recording
`endOfCase` returned an empty array and I nearly wrote it up as a regression of R-210.
It was not. `skillRoll` ticks experience only when told *which skill* was used, and my
ad-hoc rolls passed a rating and a category but no `skillFamilyId` — the same omission
R-222 found in `resolveLegDialog`, reproduced by hand in a test harness. Re-rolled with
the skill named, the tick appeared and the improvement roll landed: **Medicine 56 vs
42% → +6 → 48%.** The rule was right; the probe was not naming its skill.
## PAPER HARBOUR — the criminal
A pier that closed in 1974. A Custodian team crossed nineteen hours ago on a routine
survey and their beacon is still transmitting; three of the four are still alive and
the fourth is what the beacon is. The team: **Sennen Roake**, probationary, recruited
out of a city with a caution the department has quietly lost — plus a Field Lead and a
Technical Officer.
The generator's gap, unprompted: *one agent has a connection to this place that the
assigning officer either missed or did not miss, and it surfaces when somebody uses
that agent's name before it is given.* Phase 2 forced that encounter.
**He held it.** Fast Talk 14 against 48 — a clean success — and Sowande missed the read
on 42 against Insight 32. Nobody on his team knows. The case's own pressure, produced by
the tables and resolved by the one skill a criminal has instead of composure.
**The phases went badly and the ramp is why.** 52%, then −5%, then Difficult at 26%,
then −15%. One success and three failures, Containment 10 → 7, and the window clock at
1. That is the retuned ramp doing exactly what it was retuned to do, on a team whose
best phase-lead skill was a Field Lead's Tradecraft 52.
**Then the case turned on a warrant card.** The keepers of the ground — twelve of them,
an arrangement none will describe — want *"to be relieved. Somebody, anybody, official
enough to take the duty on."* Every agent on the team is carrying exactly that, and the
probationer with the quietly-lost caution is the one who handed it over: Bargain 27,
favourable, rolled 33. **Impression −1 → +1.** He insulted them in their own language
and then gave them the first honest thing anyone had offered them in four generations.
And the one moment that was unambiguously his: the first team locked the log in behind
them, and he opened it on **11 against Sleight of Hand 50** with lock rakes and a master
key ring. The improvement rolls afterwards landed on Fast Talk and Sleight of Hand —
the only two things he actually did.
### R-246. A fumbled greeting cost nothing
`contactOutcomes` has said `fumble: { signal: -1 }` since the social pillar was built,
and impression was adjusted **only inside the `offer` branch**. So the −1 was computed
on every botched greeting and thrown away: a team could insult a group in their own
language all afternoon at no cost, and only a formal offer could ever move how they
were seen. Found by Roake fumbling a signal on 99 and the standing not moving.
Impression now falls on a fumble at any stage and still only rises on an offer. The
narrower fix matters: `read`'s delta counts **facts learned**, not standing, so "apply
the delta always" would have paid the team impression for doing research. Verified by
rolling until a fumble landed — 99, delta −1, impression 0 → −1, standing −1.
### Two notes on the trade, and one on my own play
**A probationary criminal is out-sneaked by a career Field Lead** — stealth 47 against
60 — because trade skills are rated on the agency tier bands. It is defensible (a
probationer is a probationer, and at officer tier the criminal would sit at 45–60) but
it is worth deciding deliberately: a burglar recruited last month has been picking locks
for fifteen years, and the argument for giving trades a tier-independent band for their
*core* is the same argument that says a newly recruited doctor is already a good doctor.
Left as it is; flagged rather than changed.
**I invented a step.** After the extraction phase resolved — *"you are out, but not
cleanly"* — I rolled Transposition for all three to get them home, and all three failed
on ratings of 1, 1 and 6. That was not a step the case had; the phase had already put
them out. Recorded because the rolls happened and the log should say they did not mean
anything.
## Consolidation pass, after a Codex review
Codex reviewed the last week's work and recommended stopping to consolidate before
adding more mechanics. It was right, and four of its findings were real defects rather
than style notes. I verified each against the source before acting on any of them.
**Trade expertise was coupled to agency rank (Codex #1).** This is the thing I flagged
myself after the PAPER HARBOUR play-through and then left alone, which was the wrong
call — flagging a defect is not the same as fixing it. Trades used the tier bands, so a
probationary burglar came out at 35–50% and a veteran one at 65–80%, as though fifteen
years of picking locks were something the department conferred. `TRADE_BANDS` is now
fixed at 50–65 core and 30–50 support at every rank, and **induction now scales with
service instead** — 20–32 probationary up to 45–60 veteran, because that half genuinely
is the department's. The two axes were the wrong way round. Measured after: a criminal's
stealth 60 at probationary and 69 at veteran, the difference now coming from bonus
points rather than the band; induction tradecraft 27 → 55.
**Bonus points were sprayed across every skill in the game (Codex #2).**
`grantFullSkillList` runs immediately before the bonus-point loop and puts all
fifty-nine skills on the sheet, and the loop picked from `actor.items` — so a veteran's
ninety points landed anywhere, including skills the posting has nothing to do with. The
comment two hundred lines above has always said "spread over the core skills". It never
did. Now confined to a `trainedKeys` set built from the posting or trade plus induction.
Verified on a veteran Containment officer: ten skills raised, all ten trained.
**"Random species" was not random (Codex #3).** The dialog offers *Random (rolled
separately for each)* and then passed `"baseline"` into both generators, which support
random perfectly well. Twelve rolls now produce baseline ×8, vesh ×2, cadence ×2.
**No behavioural tests (Codex #4).** The strongest point in the review, and the one I
would not have raised myself. The three guards are static — they prove the tables agree
and that no rule is written twice — and **not one of them can tell whether a rule is
read**, which is the only kind of defect this project has ever shipped. R-246 was
confirmed by rolling a hundred and twenty times waiting for a 99. That is not evidence,
it is a hope.
`tools/check-behaviour.mjs` now runs 28 deterministic tests, gated into the build
alongside the other three, covering R-231, R-232, R-243, R-245, R-246, graded defences,
the lamp rule and the shape of every posting and trade. Proved it bites by re-breaking
R-246 and watching the build refuse.
**The register moved out of the engine.** `ROLES`, `TRADES`, `INDUCTION`, `TIERS` and
the issue lists are now `postings.mjs`, a Foundry-neutral module the engine, the
rulebook generator, the guards and the tests all import normally. They were previously
pulled out of `ringbrp.mjs` with a regular expression and `eval` — which worked, and was
a trap: reformat a table and every check depending on it stops checking without saying
so.
**And the one that only a browser could catch.** My first version of that extraction
used `export { … } from "./postings.mjs"`, which re-exports without creating local
bindings — so `ROLES` was undefined inside the engine and the system threw on `init`.
`node --check` passes that happily. Loading it in Foundry does not.
Smaller items, all as Codex described them: the contact doc comment still said "only
OFFER moves standing" after R-246 changed it; `check-kits` accepted `mas` and `app` as
characteristics, which are the *display names* of `siz` and `cha`, so a posting
declaring one would have passed the guard and produced an undefined characteristic —
latent only because nobody had tried it, and now derived from `CHARACTERISTIC_DICE`
rather than retyped; `package.json` said 0.7.0 against the manifest's 0.8.0; and the
README's counts were three revisions stale, so they are now generated by
`tools/update-readme.mjs` at build time between markers.
**Deferred, with reasons.** Splitting `ringbrp.mjs` — 6,700 lines — into generation,
contacts, combat and UI modules is right and I have not done it. It is a multi-session
refactor whose whole risk is silent breakage of exactly the kind the extraction above
just demonstrated, and it wants doing when there is nothing else in flight, not at the
end of a session that has already moved four subsystems.
**On the method.** Codex's closing point is the correct one and I am adopting it: the
play-throughs find integration failures that static review misses, but the narrative log
should sit *above* executable evidence rather than instead of it. Every numbered defect
that can be reproduced without a browser now has a test. Those that cannot — anything
inside the generators, the sheets or the contact engine — are named in the test file's
header as out of its reach, so the suite does not look more complete than it is.
## PAPER WINDOW — the starter scenario, as a Foundry Adventure
Codex's six-player introductory case is now in the system as an importable scenario
module. **Adventure**, not a journal: a scenario that arrives as nine loose documents a
GM has to assemble is a scenario that gets read rather than run.
`ringbrp.starter` contains one Adventure holding:
* the **GM scenario** in twelve pages, in running order;
* the **seven player handouts** as their own journal at ownership OBSERVER, so they can
be revealed one at a time without the GM changing permissions on seven pages before
the session — the GM journal stays hidden;
* the **six pregens**, built through the same `buildActor` path the shipped pregens
use, so they arrive with full skill lists, kit, hit points and linked tokens;
* the **cast** — Evelyn Vale, both Lorna Vales, Gareth Moss and the Cancellation Man —
as statblocks a GM can drag onto a scene;
* the **Borrowed Authority** as a real credential item with scope, Trust 2 and its three
red flags, rather than a paragraph in the brief;
* the **case file** already at Containment 8, Resources 8, Clearance 6 with its four
phases loaded at the scenario's stated difficulties and modifiers.
**The pregens are fixed, not rolled.** Ratings sit at the middle of each band rather
than being generated, because a pregen that varies between installs is not a pregen and
six tables running the starter should see the same six agents. Verified after a live
import: Nia carries the team's highest Tradecraft at 53 as the text requires, Callum's
riot shield is **slung** so raising it is still the player's decision, and the three
probationers show the trade/induction split the scenario is built to teach — Elin
Medicine 58 against an induction of 26, Owen Electronics 58, Sennen Sleight of Hand 58.
The expansion from a posting or trade into a full sheet reuses the register and the
induction rule rather than restating either, so the pregens cannot drift from the
generator: change `TRADE_BANDS` and the starter's doctor changes with it.
**Verified by importing it rather than by building it.** The pack builds, the Adventure
is well formed, the import completes with no errors, the case roster resolves through
stable keys after import (raw ids do not survive one), the handouts come in
player-visible and the GM text does not, and **phase one runs straight out of the box** —
Nia leading the cordon on Tradecraft 53. Then everything imported was deleted again,
which the pack does not notice.
## PAPER WINDOW — played with the shipped pregens
Ran the starter end to end with the six pregens exactly as they import. It closed, and
it found four defects in what I had shipped an hour earlier — all four in the scenario
data rather than the engine, and every one of them silent.
**R-247. The Borrowed Authority did not work, and said nothing.** I wrote the
credential's scope as `"operate"`, which is not one of the five scope tiers. An
unrecognised scope ranks 0 — *nothing* — so `scopeStanceFor` returned "impossible" for
a routine site inspection by a surveyor at a demolition site. No error, no warning: the
credential was simply inert, and the first scene of the starter is the Borrowed
Authority tutorial. Fixed to `"routine"`, guarded in check-kits, and covered by a test
that asserts an unrecognised scope makes everything impossible so the failure mode is
documented rather than rediscovered.
**R-248. The contact subsystem was unreachable — in the scenario built around it.** I
gave the case file a standing record for the Returned Clerks and no group *item*.
`resolveContact` looks up the group and returns null without one, so READ, SIGNAL and
OFFER did nothing at all. PAPER WINDOW's central scene is a three-stage negotiation with
those clerks. The group is now data in `scenario-starter.mjs`, consumed by the builder,
and a test asserts every standing on a shipped case file has a group behind it.
**R-249. A phase kind written to the wrong field.** I wrote `kindId` on the legs;
`resolveLegDialog` reads `leg.kind` and falls back to a regular expression on the terrain
prose. It worked only because "Extraction — closing the register" happens to begin with
the word. Rename a phase and the return clock stops moving.
**And one that was not a defect.** Phase four resolved as Difficult when the scenario
table says Average — because Resources had reached 0 and destitution makes every
remaining phase Difficult. That is the rule working, and it was the best beat in the
session: they ran out of supplies and the way home doubled in difficulty.
### How it actually played
The credential carried them in and then the stretch for an asbestos exclusion spent it
to **Trust 0** — two hours bought, and nothing left for later. Phases one, two, three and
four **all failed**, which the scenario is explicitly built to survive: Containment 8 → 2,
Resources 8 → 0, Clearance 6 → 5, and the case still closed.
The investigation split cleanly across four players and three of the four discoveries
landed. Amira's READ turned up **both** taboos on one roll — Read the Room granting the
extra fact — and Elin **criticalled on 01** for the desire. SIGNAL and OFFER at
favourable took the clerks to **Standing 2**, which fired Evelyn's rewards automatically
and put the clean settlement on the table.
The Cancellation Man opened with a **critical on 01** against Callum, whose riot shield
was raised and whose arm therefore had 10 points of armour. A critical ignores armour
entirely: 10 through, Major Wound, 15 → 5. That is the shield lesson and the graded
defence lesson in a single roll, in round one, without any prompting from me.
Then the closing scene did the thing the scenario promises: **two of six requirements
succeeded and the settlement still completed.** Nia's authority failed and spent the
Clearance anyway; Callum's containment failed for a point; Elin certified them and is
now personally responsible for reviewing all 317 cases; Owen's repair worked and took
the last of the spare cells. Sennen's **special on 07** recovered the crown stamp with
the procedure intact, using the +20% Evelyn had given him at Standing 2 — a reward
granted by the contact engine, spent in the final scene. That chain is the whole design
working unassisted.
### One thing the starter needs saying out loud
**Nobody on this team can aim the return.** Transposition is trained by exactly one
posting in the register — the Anchor Officer — and none of the six has it, so all six
roll the printed 1%. The rule that failure gets an agent home *near* the target and
never strands them is what makes that survivable, but asking a GM to roll six dice that
are all going to fail teaches nothing. The extraction page now says so: narrate it, roll
only if somebody wants a clean arrival, and note at debrief that the department sent six
people to a crossing and no anchor. That is the campaign rather than an oversight — but
it is a deliberate choice a GM should be able to see, not a hole they fall into.
## BLACK PLATFORM replaces PAPER WINDOW as the starter
The starter is now an action case: a train running through a station whose rails were
taken out in 1976, two engineers aboard, and eleven minutes until it comes back. Go
down, take the platform, board, rescue, fight forward along a moving train, stop it.
It is a better first case than PAPER WINDOW for the same reason it is simpler. PAPER
WINDOW's centrepiece was a three-stage negotiation and a six-signature settlement —
excellent, and a lot to hold in your head in hour one. BLACK PLATFORM is a straight
line with one clue scene and no social subsystem, which also means it cannot reproduce
R-248: **no groups, no standings, nothing to leave unwired.**
**Every agent can fight, deliberately.** The scenario sets floors — a firearm at 42% or
better, a melee weapon at 40%, Brawl 40%, Dodge 40% — so that nobody is a spectator when
initiative is rolled on their first evening. The floors only ever RAISE a rating, so a
posting already better at something stays better: Callum's shield comes out at 53 rather
than the floor's 45, and Owen is still far better at engineering than at fighting.
Verified on import, all six.
The build now handles three things it could not before: **combat floors**, **extra
weapons** issued for a case, and a scenario with **no Borrowed Authority at all** — the
station is already shut to the public, so there is nobody to be somebody else in front
of. That last one had been assumed to exist and would have thrown.
Two new tests: every floor must name a real skill (a floor on a skill the catalogue does
not have would put a number on the sheet that no rule could ever reach), and every
starter agent must actually clear the fighting floors the handout promises them.
## The dialogs now ask questions
Sixty-two strings reworded. The dialogs were naming mechanics at people:
| Before | After |
|---|---|
| Situational modifier % | Anything else helping or hurting? (%) |
| Target cover | Is the target behind anything? |
| Difficulty | How hard is it? |
| Fire | How are you firing? |
| Range | How far away? |
| Light | How much can you see? |
| Applies itself | Working for you already |
| Difficult (÷2) | Difficult — half your skill |
| Beyond twice the long band. The shot is impossible. | Too far away to hit. Get closer or use something with more reach. |
| That is Impossible. No roll is permitted. | That cannot be done at any skill. Try something else. |
The principle: **a label should ask the question, and a hint should say what it does to
the number.** The difficulty ladder now says what it does — "Easy — double your skill",
"Difficult — half your skill" — rather than printing an operator. Notifications say what
to do next instead of what went wrong.
The generator dialog got the same treatment: "Recruited as" is now "Where do they come
from?", "Tier" is "How senior are they?", and the hand-roll option explains what it
actually costs you — "You keep exactly what you roll — no adjusting the best one, no
species bonus."
Checked afterwards that every localisation key still referenced by the engine or a
template exists; the six apparent misses are Handlebars concatenation prefixes rather
than lookups.
## The player primer, and the journals finally look like the game
Ten pages a player reads before session one, in their own compendium at OBSERVER
ownership: nine of setting and one cheat sheet. Plus the branding that all four
journals in the system had been going without.
**The voice was the first thing to fix.** The draft called the organisation "the
Bureau" throughout. Everything else in the game — twelve rules pages, the starter, 927
localisation strings — calls it *the department* or *the agency*. One name, and it is
not Bureau.
**Where the draft was thin, it got filled rather than padded.** The character section
now carries the seven principles as an ordered list a player can actually work from;
"what it costs" gets the real Coherence band table rather than a paragraph describing
one, and says what the anchor in the kit is for; and the recruitment page separates
*what continuity means in practice* from *what it is not*, because "it is not a
superpower" is the single most important sentence for a new player and it was buried.
**The cheat sheet's numbers are computed, not typed.** Hit points, Major Wound, damage
modifier, the defence penalties, the range ladder and the Coherence bands all come out
of `rules.mjs` and `lang/en.json` at build time. A hand-typed cheat sheet would have
been the sixth time this project shipped a number that disagreed with the code — and it
would have been the one the players were holding. It closes on six questions new
players actually ask, including "am I going to die?" answered honestly.
### R-250. The design tokens were never on `:root`
The journals rendered as Foundry's default parchment, and chasing why took four wrong
guesses that are worth recording, because each one looked right.
1. **The hook name.** v14's journal sheet is an ApplicationV2: the hook is
`renderJournalEntrySheet`, not `renderJournalSheet`.
2. **`element` is the node.** On AppV2 it is the root element, not a jQuery wrapper.
Reaching for `[0]` silently returns the first *child* — which is how the first
version appeared to work, put the class on a header button, and changed nothing.
3. **The page container.** There is no `.journal-page-content` in v14. The prose sits
directly inside `article.journal-entry-page`.
4. **And then the real one.** With the class on the right element and the selector
matching, `font-size`, `line-height` and `padding` from my rule were all applying —
and every colour was not. `--rb-deep` resolved to nothing. The palette is declared
on `.ringbrp-sheet, .ringbrp-chat, .ringbrp-window` and **never on `:root`**, so a
journal carrying a new class inherited no tokens at all. Every `var()` silently
became nothing, which CSS treats as "unset" rather than as an error.
The tell was that the *numeric* properties from the same rule worked while the coloured
ones did not. A rule that half-applies is not a specificity problem, and I spent three
attempts treating it as one — including bumping the system version on a cache theory
that was wrong.
Adding `.ringbrp-journal` to the token block fixed all of it at once.
**Now branded:** the primer, the generated rulebook, the starter's GM text and its
handouts. Scoped by a flag, so another module's journals in the same world are
untouched. The contents rail is the yellow-on-navy of the sheet tabs, read-aloud gets a
spot-colour margin rule, diagrams are monospaced in a bordered block, and the cheat
sheet is a grid of cards so it can be skimmed under pressure rather than read.
Version bumped to 0.9.0 — which, incidentally, is what makes Foundry re-fetch the
stylesheet for anyone updating.