Measured in simulate.mjs. For each creature, the pack size whose spread-fire win rate lands nearest 50% — a fight at 0% or 100% cannot show an effect, which is why R-259's first sample was three-quarters useless — then focus versus spread, 2000 fights across three seeds. 31 of 47 creatures had a pack size that could show anything. Focus fire helped 31 of 31, by more than that row's own noise 31 of 31 times, mean +12.8 points. Best is 6x The margin, 40.4% -> 61.1%. The other 16 are unmeasurable rather than unaffected: eleven are trivial six at a time, five are hopeless in pairs. R-259's decomposition was confounded, and I wrote it while correcting a confound. It credited the defence ladder with 4-5 points by comparing dodge <=30% against dodge >=55% — but the low-dodge samples were PAIRS and the high-dodge ones TRIOS. Held at a fixed pack size, dodge is worth 1-2 points. Pack size is the driver: 8.0 points for a pair, 14-15 for three or more, then flat. What focus fire buys is that a dead creature stops attacking and a half-dead one does not attack any less. Two failures, one cause: comparing groups that differ in more than the thing being measured. The control is cheap and I did not run it until the third time. The bestiary advised concentrating fire and gave the ladder as the reason — right advice, wrong reason. It now gives the reason that survives measurement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
320 KiB
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-01fumble 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-22rounding: BRP requiresceilfor HP, Major Wound and Experience Bonus; spec usedround/floor/floor.- Major Wound is caused by one blow meeting the threshold, not by crossing a current-HP boundary.
R-22cross-referencesR-09, which is about age, not HP. Broken reference.R-14unfamiliar-specialisation transfer is catastrophically broad — would grant Science (Nuclear Physics) at 60% off Science (Biology) at 80%.R-15xeno-cap as written needs hidden per-skill learning-source provenance — i.e. it reintroduces the parallel economy it claimed to delete.R-16contains 36 rows, not 35; omits Navigate and Research.R-23encumbrance base should be STR kg (spec said MAS), with local-gravity multiplication.R-24/R-26directly contradict each other on whether tier-gap-3 needs research.R-27advancement omits the Experience Bonus from the improvement roll and mis-states the over-100% rule.P-3/P-5overstated 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.
Ringworldskill'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-25research — 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
R-24— contradictory permissions, no defined unlock scope.R-25— no step durations, difficulty modifiers, progress floor or ownership.- Species, pursuits, equipment, armour, weapons, defects and vehicles are referenced but have no schemas and no data.
Agreed direction for v0.2
- Delete
TECH_TIER_GAPas written; replace with technological-tradition familiarity plus explicit material/weapon traits. - Rebuild Research as typed, expedition-generating steps producing Discoveries.
- Restore
Ringworldskill's real remit. - Add the expedition/travel procedure (Supply / Power / Vehicle Integrity).
- Add a first-contact procedure.
- 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
- "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." - "Deliberate: may aim across consecutive rounds, maximum +30%" is a no-op. Deliberate already reaches +30% from a single round of aiming. Dead rule.
- 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.
- 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
- Move contact before the leg-3 roll; make immediate rewards depend on impression within the encounter while permanent Standing still moves only ±1.
- 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).
- Give every weapon range bands in its own gear entry.
- Give NPCs gravity origin and marksmanship style.
- Make research completion set the brone's repair to Average at −20 familiarity; operation failure costs 1 Power, not 4.
- Fix the §1 modifier-order authority and apply it consistently to every worked example.
- 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:
- fails if anything outside
rules.mjsre-defines a canonical rule, - asserts the builder's aliases are the canonical functions rather than copies with the same name,
- 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
manipulationMajor Wound, whose own text reads "−30% Manipulation", applies 0 - a
mobilityMajor 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.bleedwas derived and displayed, and no code ever subtracted it. At an actual Foundry table bleeding did nothing. - Nothing stopped it.
firstAidhealed 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:
- 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.
- 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.
- 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:
- Fixed layout sizes every column from the first row — the header row, where
the font is 0.62rem.
width: 3.6emtherefore resolved to about 39px, too narrow for "RANGED", so the headers overflowed into each other and the ranges wrapped. .rb-loc-cellwasdisplay: flexon a<td>, which takes the cell out of table layout altogether — it had been there since before these columns existed.- 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 isbaseline, nothominid;buildActornow 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-rulesfails the build if anyspeciesProfileintemplate.jsonis not a key oflocationTables. Verified by reintroducing "hominid": the build stops withspeciesProfile "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:
- 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.
- 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.
- 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
ROLESinringbrp.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
- 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
buildActorpath 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.
- The hook name. v14's journal sheet is an ApplicationV2: the hook is
renderJournalEntrySheet, notrenderJournalSheet. elementis 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.- The page container. There is no
.journal-page-contentin v14. The prose sits directly insidearticle.journal-entry-page. - And then the real one. With the class on the right element and the selector
matching,
font-size,line-heightandpaddingfrom my rule were all applying — and every colour was not.--rb-deepresolved to nothing. The palette is declared on.ringbrp-sheet, .ringbrp-chat, .ringbrp-windowand never on:root, so a journal carrying a new class inherited no tokens at all. Everyvar()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.
R-251. The largest combat change the system has had, measured four commits late
ddc4f99 routed every ordinary blow through the hit location table. Until then the
locations were a hazards-only subsystem — falling, fire, explosions and automatic fire
rolled a location, and every sword and pistol did not. That commit is the reason the
rest of this work exists, and nobody measured what it did to the game until now.
check-lethality did not notice, and the way it failed is the useful part. Twenty-six
of forty-six creatures moved. Not one of them moved more than two points of wipe rate,
the guard allowed six, and so it reported OK for four commits while comparing against
numbers from a version of combat that no longer existed. A tolerance sized for sampling
noise, in a simulation that has none, is just a blindfold with a rationale.
Isolated by replaying 322389b and ddc4f99 against the same party, seed and counts:
| creatures whose numbers moved | 26 of 46 |
| deadlier | 3 |
| less deadly | 8 |
| unchanged wipe rate, moved elsewhere | 15 |
| mean change in wipe rate | −0.17 points |
The largest single moves were the courier (6% → 3.5% wipe), the arrears (7.5% → 9.5%), the stanchion (26% → 28%) and the long walker (68% → 66.5%).
So the answer is reassuring, and it is worth having written down rather than assumed: sending every blow through a location redistributed lethality without raising it. Creatures with one big attack got slightly less dangerous, because a blow that used to come straight off a single hit point pool now has to beat the armour covering wherever it landed. Creatures that attack often got slightly more dangerous, because they now accumulate damage in a limb and disable it. Across the bestiary those cancel to within a fifth of a point.
Two corrections to the record, both mine, both from explaining before measuring:
- I reported that adding a creature shifted a shared random stream and perturbed its
neighbours' numbers. It does not.
measure()builds its own generator per call; inserting a creature ahead of the barghest changes 0 of 47 entries. The guard's own claim about this was right and mine was wrong. - I then said the file's claim was untrue in a commit message. It was true.
The baseline recorded in e5dc9b5 already carries these numbers. What was missing was
this page.
R-252. One seed's luck, spread across forty-seven creatures
Every creature was measured against seed 11. That made each one independent of its neighbours — the property I had wrongly claimed was broken in R-251 — but it also gave all forty-seven of them the same two hundred dice. Independent of position, identical in luck.
Each creature now derives its own seed from its key (FNV-1a mixed into the base seed), which is still deterministic, still reproducible from the base seed alone, and still local: adding a creature changes 0 of 47 existing rows, measured, exactly as before.
What it was worth is smaller than the argument for it, and worth writing down honestly:
| shared seed 11 | per-creature | |
|---|---|---|
| mean wipe rate across the bestiary | 5.67% | 5.67% |
| mean agents down | 0.538 | 0.522 |
| rows that changed | — | 34 of 47 |
So seed 11 was not biasing the book. There was no systematic luck to remove, and anyone expecting the creatures to look collectively softer or harder afterwards would have been disappointed. What the change does is decorrelate: the errors in individual rows no longer all come from the same draw.
The more useful number fell out of the comparison. Individual creatures moved by up to four points of wipe rate — the courier 3.5% → 7.5%, the long walker 66.5% → 62.5%, quarantine unit 84.5% → 88.5% — purely from being handed different dice. That is the size of the sampling noise sitting inside any single recorded figure at 200 runs.
Which means the two things this file records must be read differently, and the header has always said so:
- As a regression fingerprint the numbers are exact and now compared exactly. Any movement is a real change to the rules or the creature. That is what the guard is for.
- As a description of a creature a single figure carries several points of slack. "The courier wipes 7.5% of parties" is one sample, not a property. Raising the run count is the only thing that narrows it; more seeds does not.
seedMode is recorded alongside the numbers, because changing how a seed is derived
moves every row without changing SEED itself, and a baseline from the old scheme would
otherwise have been compared against this one silently and wrongly. It is refused now.
R-253. Ten times the runs, for two and a half seconds
R-252 established that a creature's recorded figure carried several points of slack at 200 runs, and that only more runs narrows it. So: 2000.
Measured by taking every creature under four different base seeds and looking at how far the four answers sat apart. That spread IS the noise, and it is the thing a GM reading the bestiary is unknowingly trusting:
| mean spread | worst creature | |
|---|---|---|
| 200 runs | 1.28 points | 11.5 points |
| 2000 runs | 0.36 points | 2.7 points |
The worst case is the number that mattered. At 200 runs one creature's published wipe
rate could be eleven points away from the same creature measured with different dice —
on a figure printed in docs/BESTIARY.md for a GM to plan an evening around. It is now
under three.
What it corrected in the published book: 19 of 47 figures moved, mean 0.35 points, and the arrears went 6% → 8.9%, the supporter 44% → 46.8%, the nuckelavee 13.5% → 16%. Each of those was a sampling artefact being printed as a property.
The cost is the part worth recording, because it is the reason this was never done. I had guessed "maybe a minute of build time" when suggesting it, which was wrong by a factor of thirty and would have been a fair reason to refuse:
check-lethality alone 0.29s -> 1.88s
the whole nine-guard suite 2.55s
Ten times the statistical work for one and a half seconds. The guard had been carrying a tolerance of six points, and then an exact comparison against figures that could move eleven, for want of a measurement that cost less than the time to read this paragraph.
R-254. Grounding it was a forty-three point mistake
Two commits in a row shipped a defect that neither of their guards could see, and it took an unrelated question — what would an aimed shot at −30% be worth? — to find it.
R-95 made losing a wing change the animal. R-96 took the hide off the wing so that losing one was possible at all. Separately each was right and each was measured. Together they gave the party a soft target and then deleted it the moment the party hit it:
winged 6/20 ranged faces land on armour 0 (a bare wing)
quadruped 0/20
A service round on borough ground averages 3.75 after halving: 3.75 through a bare wing, 0 through hide 7. So putting the creature on the ground took the party's chance of winning from 44.9% to 1.4% — and its own tactics tell the GM that grounding it is "the fight the agents can actually win".
The aimed shot measurement is what exposed it. Every configuration made the party worse, and the worst was the most aggressive: aiming at a wing at −20% grounds the creature in 99% of fights and halves the win rate to 10.1%. Buying the objective reliably was the fastest way to lose. That only makes sense if the objective is a trap, and it was.
It also retires a number I reported at 1.7.3: that grounding was "worth +31 points", measured as win-rate-if-grounded against win-rate-if-not. That was selection bias — parties that managed to ground it were the ones shooting well. Forcing the grounding reveals the causal value, and it was deeply negative. A correlation of +31 and a causation of −43 out of the same mechanic is worth remembering the next time one of these numbers looks encouraging.
The fix. A heraldic beast is armoured the way it is drawn: across the back and the
flanks. Down on its belly, half that hide is no longer between you and it, so
exposedHideFor halves natural armour while system.grounded is set. Hide 7 becomes 3,
which restores something an ordinary round can get through without pretending a grounded
animal is harmless.
| flying | grounded | grounding is worth | |
|---|---|---|---|
| on borough ground | 45.3% | 51.7% | +6.4 |
| off borough ground | 90.6% | 97.9% | +7.3 |
Modest on purpose. Grounding it should help, not decide; the borough is still the real answer. Of the options measured, losing its damage modifier entirely would have been worth +26.5 and made the wing the whole fight.
The guard, and the two mistakes in building it. check-anatomy now measures what an average round gets through before and after, and fails a flyer that is harder to hurt on the ground than in the air. The first version probed at a single 4-point round and reported that the fix had failed — because 4 is inside the one narrow band where a bare wing beats a halved hide. Below 5 damage the creature really is better off on the ground; above it, much worse. Integrating across the range 1–12 gives 2.83 flying against 3.75 grounded and reads the design correctly.
It is also deliberately one-sided: it catches grounding being a setback, not grounding being too generous. Stripping the hide to zero passes it. That is a balance question and this is a correctness guard, and conflating the two is how the six-point tolerance in check-lethality came to hide a real change for four commits.
R-255 — the belly rule was measured against the wrong question, and undersold
R-254 called +6.4 points "modest on purpose" and I left it there. That was the wrong reading of my own number. +6.4 measured whether grounding helps; the design question was whether the borough still matters, and on that the belly rule had quietly gone too far — the gap between fighting the supporter on borough ground and off it had fallen from 60.6 points (1.7.8) to 44.9 (1.7.9). Half-damage from every departmental weapon is supposed to be the fight's central problem. Compressing the penalty for ignoring it by sixteen points is not modest, and calling it modest because a different statistic looked small is the same error as R-251: reporting the measurement I happened to take instead of the one the decision needed.
The fix: hide 9, grounded 4, vital exempt. Three changes that only work together.
- Hide 7 → 9. Restores the weight the belly rule removed, on the axis that is supposed to hurt: being shot at with departmental kit.
- Grounded halves it, as R-254 — 9 becomes 4, still a number an ordinary round beats.
exposedHideFornow takes the location's kind and returns natural armour unhalved forvital. A beast on its belly is not presenting its chest to the floor; that plate is facing you in every posture. Mechanically it keeps one hard target so grounding is a change of problem, not a solved fight.
| flying | grounded | gap to off-borough | |
|---|---|---|---|
| 1.7.8 (no belly rule) | 1.4% | — | 60.6 |
| 1.7.9 (hide 7, halved) | 45.3% | 51.7% | 44.9 |
| 1.7.10 (hide 9, grounded 4, vital exempt) | 28.4% | 68.4% | 56.2 |
Grounding is now worth 40 points and the borough is worth 56 again. Predicted 28.4 / 84.3 / 55.9 before building; measured 28.4 / 84.6 / 56.2.
Negative test. Exempting every location rather than only the vital makes check-anatomy guard 6 fail — "an average round gets 2.30 through it flying and only 0.50 once grounded" — which is the guard doing its job on the version of this change that would have been wrong.
Cost. The supporter moves from fifth to second deadliest creature in the book: 2.84 of 4 down to 3.38, 46.8% wiped to 69.3%, 14 rounds to 17. check-lethality refused the build until that row was deliberately re-recorded, which is the behaviour R-251 bought.
The doc gap this exposed. BESTIARY.md printed the new hide of 9 and said nothing about
it halving, because the body-plan blurb was written before the rule existed. A rule that
lives in rules.mjs and not on the page the GM reads at the table is the project's
signature defect wearing a different coat. The winged note now states both halves: the
wings carry no hide, and grounding halves the rest except the chest.
One more count that was written down and never read. The README's guard block is
generated and validated against npm run check, and three sentences around it still said
"the eight guards" while nine ran. update-readme now rewrites the count word as well as
the block. The comment warning that a hardcoded list goes stale was sitting four lines
above the prose that had gone stale.
R-256 — the header said 9 while the body said 4
Verifying R-255 on a live sheet showed the rule working everywhere it was built and contradicted in the one place a GM actually looks first. The header's ARMOUR field is the hide the creature HAS; grounded, the hit-location table underneath it read 4 and the header still read 9. Nothing computes off that field, so nothing was wrong in the code — it was wrong at the table, which is the only place that counts.
The field stays editable and stays 9, because 9 is the creature's hide and that is what a
GM needs to type into. Underneath it, when system.grounded is set, the sheet now states
what is in play: DOWN: 4 · CHEST 9, from exposedHideFor rather than from a second
opinion, with the reason on the tooltip.
The guard, and what it caught on the way. check-rules forbids redefining a rule BY
NAME, which misses how this actually goes wrong: nobody redefines exposedHideFor, they
write Math.floor(hide / 2) in the sheet because it is one keystroke cheaper than an
import. I wrote the wrong version first to prove the point and check-rules passed it
happily. It now fails any file outside rules.mjs that halves a hide inline, with the
file and line; the check files are exempt, because independently stating the expected
value is what a test is for.
Open, and worse than what I fixed. Writing that guard turned up two copies of a
different armour rule. armourAgainst — critical ignores armour, special halves it —
lives in ringbrp.mjs:414, not in rules.mjs, so the authority never covered it; and
tools/simulate.mjs:267 has its own inline version rather than importing it. They agree
today, so no published number is wrong. But every figure in BESTIARY.md and the whole
lethality baseline comes out of the simulator, so the day someone changes how a special
hit meets armour, the game changes and the measurements do not, and the guards will all
still pass. That is R-251 with a longer fuse. Not fixed here: moving a rule into
rules.mjs and re-pointing the simulator at it is a change to the measurement pipeline
and wants its own before-and-after.
R-257 — the harness had its own copy of how armour works
Left open by R-256 and fixed here. Two rules governed how a hit meets armour:
armourAgainst— a critical ignores armour, a special halves it, rounded downdamageAfterArmour— what is left never goes below zero
Both lived in ringbrp.mjs, which is the game, and neither was in rules.mjs, which is
the authority — so the guard that forbids redefining a rule had nothing to forbid. And
tools/simulate.mjs applied armour from its own inline copies of both, as it had since
it was written.
They agreed. That is the whole point. Nothing published was ever wrong, and no guard
could have told you they were two things rather than one. But every number in
BESTIARY.md, every row of the lethality baseline and every measurement quoted in this log
from R-251 onward comes out of that harness. Change how a special hit meets armour and
the game changes; the numbers that describe the game do not; all nine guards pass. That
is exactly the shape of ddc4f99, which changed the game and left the measurements behind
for four commits — except that this one had no six-point tolerance to blame, and would
have hidden indefinitely.
The fix. Both rules moved into rules.mjs. ringbrp.mjs imports and re-exports them,
because they are part of the public API at game.ringbrp and macros call them. The
simulator imports them. Verified live in the world that game.ringbrp.armourAgainst and
rules.mjs's export are the same function object, not two that agree.
Proof it changed nothing. check-lethality compares 47 creatures over 2000 fights each against the recorded baseline, and every one of them fights exactly as recorded — no tolerance, no drift. A refactor of the rule the harness applies to every single hit, across roughly four million resolved attacks, moved nothing. That is what makes it a refactor rather than a change.
The guard. check-rules now fails any file outside rules.mjs that halves armour or
subtracts it inline, naming file and line; check files are exempt. Negative-tested by
restoring the harness's original three lines verbatim — both fire — and by redefining
armourAgainst in ringbrp.mjs, which the older name-based guard now catches because
the rule finally lives somewhere it can be said to belong to. The two new exports also
tripped the spot-check requirement the moment they arrived, which is the authority
working as designed: eight new spot-checks, and the numbers for a special hit against
hide 9 are now stated in exactly one place.
R-258 — the firing order, and a check that was written and never run
DEFENCE_STEP is −30, so a defence is not a percentage a creature has for a round: it is
a percentage it has once or twice. The median dodge in the bestiary is 45% and goes
45 → 15 → 1 within a single round, resetting when the round does. Nothing in any
player-facing document said so, which made a real tactical decision invisible at the
table: what order the party shoots in.
Three consequences, all of them derived rather than asserted:
- only a landing hit spends a defence, so a miss strips nothing
- each landing hit makes the next attack of that round easier
- therefore the cheapest reliable weapon fires first and the one you most need to land fires last — against the supporter, that is the weapon the borough does not own
Measured in a table-side harness over 2000 fights per ordering: firing order is worth
10.7 points of win rate across the 24 permutations of one party (52.5% worst to 63.2%
best), against 6.7 points for the concentration effect itself. Leading off with the
only un-halved weapon is the single worst choice available. Those figures are NOT from
simulate.mjs and are not published — the harness ignores wound penalties, bleeding and
dying — so the bestiary states the mechanism and the tactic and quotes no win rates.
What it says is generated. The step, the ladder, the median, the distribution (13 of
47 creatures stripped by one landing hit, 25 by two, 9 by three) and the list of the
hardest to strip all come from DEFENCE_STEP, resolveBands and the statblocks.
Negative-tested by setting the step to −15 and −20: the page rewrites itself both times.
My first draft was wrong in the way this log keeps recording. It picked the dodgiest creature in the book as the worked example — 80%, which takes three landing hits to strip — directly under a sentence claiming the first attack of the round eats the dodge. True for a modest dodge, false for a good one, and I had written both on the same page. The general rule (each landing hit strips 30) holds for every creature; the overstatement did not.
And the defect underneath it. tools/bestiary.mjs --check has existed since the
document did, complete with the comment "(for the guards)", and no guard ever ran it.
A check written and wired to nothing — the same shape as every rule this project has
found computed and never read. It mattered little while the page merely restated
statblocks. It matters now that the page derives from rules: change DEFENCE_STEP and
the document silently lies to a GM. check-bestiary.mjs is now the tenth guard.
Negative-tested both ways it can fail: a rule change that makes the page stale, and a
hand-edit of the page itself. update-readme caught it as "run by the build, not
advertised" before I had added it to the list, and the README's count word went from nine
to ten on its own, which is R-256 paying for itself.
R-259 — the firing order was worth nothing, and R-258 shipped it anyway
R-258 put a tactic in the bestiary on the strength of a table-side harness: cheapest
weapon first, best weapon last, worth 10.7 points of win rate across 24 orderings. Run in
simulate.mjs — the pipeline that produces every published number — it is worth
+0.27 points, with per-seed differences from −1.5 to +1.8 against 2.6 points of
seed-to-seed noise on a single arm. It is zero.
Why the toy lied. Two reasons, both mine.
- That party had one uniquely valuable weapon — an anti-materiel rifle the borough does not own, so unhalved and worth roughly double anything else. Reordering around it had something to reorder around. The frozen party's four are worth 4.09, 3.98, 1.43 and 1.43 expected damage: two near-identical pairs, and nothing to gain by sequencing them.
- The toy used a fixed order every round. The game re-rolls Reaction every round, so over a fight every agent takes the opening slot about equally often. There is no rule for holding an action — I checked before measuring — so the order is not a choice a player gets to make. I had written table advice for a decision the rules do not offer.
The page is corrected. The ladder stays, because it is derived and true. What it now says is that you cannot choose who goes first, and why.
What IS worth doing, measured the same way. Committing a round's attacks to one target rather than spreading them, in fights that can actually move (spread-fire win rate between 20% and 80% — a fight at 0.2% or 100% cannot show an effect and three of my first four samples were pinned there):
| pack | dodge | spread | focus | gain |
|---|---|---|---|---|
| 2× Cuckoo array | 5% | 65.7% | 73.9% | +8.2 |
| 2× The apple tree man | 10% | 45.0% | 52.0% | +7.0 |
| 3× Spriggan | 45% | 48.9% | 64.9% | +16.0 |
| 2× The yield | 60% | 55.8% | 67.4% | +11.6 |
| 3× Precedent | 65% | 70.9% | 82.2% | +11.3 |
| 3× Redcap | 75% | 38.9% | 52.7% | +13.8 |
Against low dodge the gain is 7.6 points and is simply killing attackers sooner. Against high dodge it is 12.2, so about 4–5 points is the defence ladder and the rest is arithmetic that has nothing to do with dodging. Splitting it needed contested fights at both ends of the dodge range; without that control I would have credited the whole 13.8 against the redcaps to the ladder, which is the same error as R-255's +6.4.
The instrument. runFight gained two hooks, both off by default and unused by the
baseline: partySequence rearranges the party among the slots initiative already gave
them — leaving enemies where they fell, so the experiment isolates "what order the agents
act in" from "who acts before the creature" — and partyTargets: "focus" concentrates
fire. Both roll the same dice as before, so the stream is unchanged, and check-lethality
confirms all 47 creatures still fight exactly as recorded with the hooks present.
The pattern. Third time this session that my explanation was wrong and the experiment settled it, and the first where the wrong explanation had already shipped to a document a GM would read. The rule I keep relearning: a measurement taken outside the guarded pipeline is a hypothesis, not a result, and it does not go in a document until the pipeline agrees.
R-260 — focus fire, all 47, and the same confound one turn later
Focus fire measured across the whole bestiary in simulate.mjs. For each creature, the
pack size whose spread-fire win rate lands nearest 50% — a fight at 0% or 100% cannot show
an effect of any size, which is why R-259's first sample was three-quarters useless — then
focus versus spread at 2000 fights across three seeds.
31 of 47 creatures had a pack size that could show anything at all. Of those, focus fire helped 31 of 31, by more than that row's own seed-to-seed noise 31 of 31 times. Mean gain 12.8 points of win rate. Best: 6× The margin, 40.4% → 61.1%, +20.8.
The other 16 cannot be measured rather than being unaffected: eleven are trivial even six at a time (the party wins ~100%) and five are hopeless in pairs — the supporter, the quarantine unit and the long walker wipe the party at 0% before focus fire gets a vote.
R-259's decomposition was confounded, and I wrote it while correcting a confound. It said the defence ladder was worth 4–5 of the gain, comparing dodge ≤30% against dodge ≥55%. Those two groups were not the same fights: the low-dodge samples were PAIRS and the high-dodge ones were TRIOS. Pack size is the real driver.
| enemies | mean gain | dodge within that size | |
|---|---|---|---|
| 2 | 8.0 | ≤40%: 7.4 · >40%: 8.6 | |
| 3 | 14.0 | ≤40%: 13.3 · >40%: 14.2 | |
| 4 | 13.9 | ≤40%: 13.0 · >40%: 15.2 | |
| 5 | 14.7 | ≤40%: 14.7 · >40%: 14.8 | |
| 6 | 15.4 | ≤40%: 14.4 · >40%: 15.7 |
Held at a fixed pack size, dodge is worth 1–2 points, not 4–5. The gain nearly doubles from a pair to a trio and then flattens. What focus fire actually buys is that a dead creature stops attacking and a half-dead one does not attack any less; the ladder is a rounding error on top of that.
So R-259 corrected R-258's un-piped measurement and then made R-255's confound error in the correction. Two different failures with one cause: comparing groups that differ in more than the thing being measured. The control is cheap — hold the other variable fixed and look within it — and I did not run it until the third time.
The page says what is true now. The bestiary advised concentrating fire and gave the ladder as the reason, which was right for the wrong reason. It now gives the reason that survives measurement, and states that the effect roughly doubles from two enemies to three or more.