The work landed in a5c4378 under the subject "R-294", which was already
taken by the peer session's derived-source resolver. R-295 was taken too,
so renumbering to 295 as I first proposed would have collided with
something already in the log. Read the log before writing rather than
accepting the correction: it runs to R-295, both of those entries are
theirs, and mine is R-296.
The commit subject stays wrong; rewriting a pushed subject to fix a number
is worth less than the entry pointing at it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7051 lines
412 KiB
Markdown
7051 lines
412 KiB
Markdown
# RingBRP — design review log
|
||
|
||
## Round 1 — Codex review of RINGBRP_RULES_SPEC_v0.1.md
|
||
**Date:** 7 August 2026 · **Reviewer:** Codex (GPT-5.4) via `codex exec` · **Author:** Claude (Opus 5)
|
||
|
||
### Verdict headline
|
||
> "The draft has a sound BRP chassis, but it currently fails its own Ringworld test. The generic
|
||
> five-tier technology rule and generic progress-clock research system replace distinctive
|
||
> Ringworld procedures with generic sci-fi mechanics."
|
||
|
||
> "Without those changes, this remains competent generic BRP science fiction wearing Ringworld nouns."
|
||
|
||
That is exactly the failure mode the spec asked Codex to hunt for, and it found it.
|
||
|
||
### Factual errors found — VERIFIED against the source corpus
|
||
|
||
Nine of Codex's checkable claims were re-checked by Claude against `~/RingBRP/extracted/`.
|
||
**All nine were correct.** Claude's spec was wrong in each case.
|
||
|
||
| # | Codex's claim | Source check | Verdict |
|
||
|---|---|---|---|
|
||
| 1 | 1984 general HP = **CON + MAS**, not an average | Explorer Book: "hit points equal to the total of his CON + his MAS" | Codex right; `R-09` halved it |
|
||
| 2 | 1984 has **seven** hit locations (L/R arm, L/R leg, abdomen, chest, head) + general HP = 8 live values | Explorer Book: lists all seven by name | Codex right; spec said "six pools" |
|
||
| 3 | POW doubles = **open-ended reroll-and-add** for Earth humans under 100, attributed to Puppeteer breeding | Explorer Book, POW entry | Codex right; spec said "may indicate psi potential" |
|
||
| 4 | Default age die is **1D8**; 1D10 reserved for NPCs/special cases | Explorer Book: "roll 1D8 … ordinarily the gamemaster should not allow such ancient explorers" | Codex right; spec said 1D10 |
|
||
| 5 | `Ringworld` skill covers **macrostructure and physical relationships only** — explicitly "equivalent to Planetology" — not artefact identification | Explorer Book, RINGWORLD (S) | Codex right; spec's "Cthulhu Mythos slot" was a genericisation |
|
||
| 6 | BRP difficulty ladder is **Automatic / Easy (×2) / Average / Difficult (÷2) / Impossible** | BRP:UGE p.113 and glossary | Codex right; spec's +50/+20/−20/−50/×⅕ was invented |
|
||
| 7 | BRP: cannot mix Parry and Dodge in one round unless completely defensive; cumulative −30% per extra defence | BRP:UGE Dodge entry | Codex right; `R-19` omitted the restriction |
|
||
| 8 | Stunners resolve on **energy output vs target MAS**, not CON vs potency | Technology Book, STUNNERS | Codex right |
|
||
| 9 | Variable sword **cannot** cut scrith, GP hull or stasis fields, and cuts dense alloys slowly | Technology Book, variable knife entry | Codex right; spec said "ignores all non-powered armour" |
|
||
|
||
Additional errors accepted without independent check (internally consistent and checkable later):
|
||
- `R-01` fumble formula does not reproduce BRP. Correct form:
|
||
`fumble_start = 101 - ceil((101 - clamp(effective,1,100)) / 20)`
|
||
(verified by arithmetic against BRP's own worked example: skill 60 → 98–00.)
|
||
- `R-22` rounding: BRP requires `ceil` for HP, Major Wound and Experience Bonus; spec used
|
||
`round`/`floor`/`floor`.
|
||
- Major Wound is caused by **one blow** meeting the threshold, not by crossing a current-HP boundary.
|
||
- `R-22` cross-references `R-09`, which is about age, not HP. Broken reference.
|
||
- `R-14` unfamiliar-specialisation transfer is catastrophically broad — would grant
|
||
Science (Nuclear Physics) at 60% off Science (Biology) at 80%.
|
||
- `R-15` xeno-cap as written needs hidden per-skill learning-source provenance — i.e. it
|
||
reintroduces the parallel economy it claimed to delete.
|
||
- `R-16` contains 36 rows, not 35; omits Navigate and Research.
|
||
- `R-23` encumbrance base should be **STR** kg (spec said MAS), with local-gravity multiplication.
|
||
- `R-24`/`R-26` directly contradict each other on whether tier-gap-3 needs research.
|
||
- `R-27` advancement omits the Experience Bonus from the improvement roll and mis-states the
|
||
over-100% rule.
|
||
- `P-3`/`P-5` overstated the diagnosis: xenological activation spent ordinary occupation points,
|
||
not a separate currency, and was expressible on a flat sheet.
|
||
|
||
### Decision Record verdicts
|
||
|
||
| DR | Codex | Note |
|
||
|---|---|---|
|
||
| DR-01 delete impulse combat | **AGREE** | Add opposed Athletics/Pilot chase exchange for the second-by-second beat |
|
||
| DR-02 keep MAS/APP names | **AGREE-WITH-CHANGE** | Canonical engine IDs `siz`/`cha`; MAS/APP as display labels in the setting layer only |
|
||
| DR-03 consolidate skills | **AGREE-WITH-CHANGE** | Keep families but require real specialties; add Navigate and Research; publish a finite launch catalogue |
|
||
| DR-04 move ranged to Combat | **DISAGREE** | Keep firearms/energy under Perception; bows/thrown under Combat. The 1984 placement was deliberate — marksmanship as concentration, not agility |
|
||
| DR-05 xeno table as training cap | **AGREE-WITH-CHANGE** | Cap `Xenology (Species)` only; Xenology then augments other skills |
|
||
| DR-06 TECH_TIER_GAP | **DISAGREE** | Replace with familiarity: familiar 0 / analogous −20 / no analogue −50 / Builder locked until a named Discovery, then −20. Materials and weapons get explicit traits |
|
||
| DR-07 retain Research | **AGREE-WITH-CHANGE** | Rebuild as 1–5 **typed sequential steps** (field, library, laboratory, analysis). Novel alien subjects require ≥1 field step. Completion grants a Discovery, not automatic skill dice |
|
||
| DR-08 ×5 attribute rolls | **AGREE** | Lethality is controlled by HP/damage/armour, not by Idea/Luck/Stamina |
|
||
| DR-09 keep Ringworld Lore | **AGREE-WITH-CHANGE** | Keep it, but restore the narrow macrostructure remit |
|
||
| DR-10 separate mechanics/setting layers | **AGREE** | Worth the naming overhead |
|
||
|
||
### Open Question positions
|
||
|
||
| Q | Codex position |
|
||
|---|---|
|
||
| Q-01 multiplier | ×5, BRP standard |
|
||
| Q-02 hit locations | **Pools off; roll a location only when a blow causes a Major Wound**, then apply a concise consequence. Preserves severing, autodocs and variable-sword horror without seven HP tracks |
|
||
| Q-03 defence | BRP default: unlimited same-type at cumulative −30%, no mixing parry/dodge, keep missile-dodge restrictions |
|
||
| Q-04 encumbrance | STR kg; each 10 kg over: −1 MOV, −5% Physical/Manipulation/Combat; multiply by local gravity |
|
||
| Q-05 opposed rolls | Full BRP, including the loser downgrading the winner's degree |
|
||
| Q-06 playable species | Human, Kzin, Puppeteer at launch. Native guide as an NPC template; native PCs later with a warning that they change the premise |
|
||
| Q-07 automatic effects | Yes — a formal `resolution: automatic` category with explicit immunity/counter tags. Not GM fiat |
|
||
| Q-08 scale | **"Yes; this is the largest missing subsystem."** Light expedition procedure: journeys split into 3–6 legs, each with time cost, resource cost and one Navigation/Pilot test. Track exactly three expedition resources: **Supply, Power, Vehicle Integrity** |
|
||
| Q-09 Foundry | Skills as embedded Items with `familyId`/`specialisationId`/`categoryId`/`base`/`value`/`trainingCap`/`transferGroup` fields — **do not parse names**. Research as a **shared Project Item on a party/expedition Actor**, not per-PC |
|
||
| Q-10 missing | First-contact procedure; Navigate and Research skills; expedition travel; vehicle/starship scale and damage; material traits; technological traditions; the young-Earth POW rule; simweb training; gravity adaptation and zero-g; and actual species/equipment/pursuit data |
|
||
|
||
### Over-deletion (put it back)
|
||
- The four 1984 research **modes** (field, library, laboratory, armchair/analysis) — especially the
|
||
rule that major research generates a field expedition.
|
||
- `Ringworld` skill's narrow macrostructural remit.
|
||
- Young-Earth open-ended POW; simweb training; local-gravity encumbrance; gravity adaptation.
|
||
- Location **consequences** — losing them guts variable swords, autodocs and transplant resistance.
|
||
- The shared handling group linking flashlight laser and variable sword.
|
||
- Publication and assimilation of xenological discoveries back in Human Space as a downtime reward.
|
||
|
||
### Under-deletion (still too complex)
|
||
- Full BRP characteristic-based category modifiers — six calculated sheet values. Use BRP's
|
||
Simple Skill Category Bonuses, or drop category bonuses entirely.
|
||
- `R-14` — recreates root-skill leakage in a worse form.
|
||
- 36 skills plus unbounded free-text specialties can exceed the original sheet burden.
|
||
- `R-25` research — up to 50 progress points, repeated rolls, two difficulty mechanisms.
|
||
- Five universal tech tiers — "an unnecessary taxonomy with little setting truth".
|
||
- Pursuit packages plus separate allocation pools. Give each pursuit a fixed package plus at
|
||
most three player-selected increases; do not reopen point-buy per career.
|
||
- Use BRP's "remove statements" option as the default combat mode: declare and resolve on DEX rank.
|
||
|
||
### Foundry blockers
|
||
1. `R-24` — contradictory permissions, no defined unlock scope.
|
||
2. `R-25` — no step durations, difficulty modifiers, progress floor or ownership.
|
||
3. Species, pursuits, equipment, armour, weapons, defects and vehicles are referenced but have
|
||
no schemas and no data.
|
||
|
||
### Agreed direction for v0.2
|
||
1. Delete `TECH_TIER_GAP` as written; replace with technological-tradition familiarity plus
|
||
explicit material/weapon traits.
|
||
2. Rebuild Research as typed, expedition-generating steps producing Discoveries.
|
||
3. Restore `Ringworld` skill's real remit.
|
||
4. Add the expedition/travel procedure (Supply / Power / Vehicle Integrity).
|
||
5. Add a first-contact procedure.
|
||
6. Fix all arithmetic and BRP-conformance errors listed above.
|
||
|
||
### Outstanding disagreements from round 1, needing the human owner's call
|
||
- **DR-04** (ranged weapons: Perception vs Combat category) — Claude proposed Combat for BRP
|
||
conformity; Codex wants Perception preserved for setting fidelity. Genuine design split.
|
||
- **DR-06** (tech gap model) — Claude's tier taxonomy vs Codex's familiarity model. Codex's is
|
||
better; noted for adoption unless the owner prefers the simpler tier table.
|
||
|
||
---
|
||
|
||
## Round 2 — Codex review of RINGBRP_RULES_SPEC_v0.2.md
|
||
**Date:** 7 August 2026 · **Reviewer:** Codex (GPT-5.4) · **Author:** Claude (Opus 5)
|
||
|
||
### Headline — the round-1 verdict is reversed
|
||
|
||
> **"No, it is no longer generic BRP sci-fi wearing Ringworld nouns."**
|
||
|
||
Evidence Codex cited, all mechanical rather than cosmetic: open-ended Earth-human POW expressing
|
||
Puppeteer breeding (`R-05a`); age/longevity/multi-pursuit generation producing Known Space
|
||
characters (`R-09`, `R-29`); Megastructure Lore with its exact 1%-on-arrival macrostructural
|
||
role (`R-17`); Ring distance consuming Power the local civilisation cannot replace (`R-31`);
|
||
field expeditions producing Discoveries that unlock specific Builder functions (`R-25`/`R-26`);
|
||
publication creating a return-to-Human-Space loop (`R-26a`); scrith, GP hull and stasis fields
|
||
changing what weapons can physically do (`R-24c`).
|
||
|
||
> "The distinctive spine exists. The problem is now execution, not identity."
|
||
|
||
### CRITICAL — v0.2 defeats the owner's own ruling
|
||
|
||
`R-12a` made **Simple Category Modifiers the default**, and BRP's simple modifiers use the
|
||
**primary characteristic only**. So the default Ranged Combat bonus is `DEX/2` — and **POW and
|
||
CON contribute nothing**. The owner ruled that ranged weapons use DEX *and* the Perception
|
||
characteristics. The full-modifier table honours that; the default does not.
|
||
|
||
Codex's fix, adopted: **Ranged simple bonus = `ceil((DEX + POW + CON) / 6)`**, and by symmetry
|
||
Melee = `ceil((STR + DEX) / 4)`. Must be corrected in v0.3.
|
||
|
||
### Regression check — round-1 errors
|
||
|
||
**FIXED:** fumble formula · difficulty ladder · opposed downgrade · POW rule · HP source and
|
||
rounding · broken cross-reference · `R-14` transfer breadth · `R-15` xeno-cap provenance ·
|
||
`R-17` Ringworld remit · `R-19` parry/dodge exclusivity · `R-23` STR base · `R-24`/`R-26`
|
||
contradiction · research architecture and reward · stunner and variable sword · `P-3`/`P-5`
|
||
retraction · monofilament transfer group · publication reward.
|
||
|
||
**NOT FIXED / NEW PROBLEMS:**
|
||
| Issue | Detail |
|
||
|---|---|
|
||
| `R-12a` | Defeats the owner's ranged-weapon ruling (above). **Highest priority.** |
|
||
| `R-27` simweb | Invokes "special failure", but `R-01` has no special-failure band. Map to fumble or the rule is unusable. |
|
||
| `R-27` experience | Allows experience on "any skill" while also saying Research is the only route past 100% for Mental skills. Add per-skill `experienceEligible`. |
|
||
| `R-25` library hours | Should be **100 hours**, not 200. *(Verified against source: "Normally, the time needed is 100 hours per step.")* |
|
||
| `R-25` multi-researcher | "ANY success completes / ANY fumble does not" is self-contradictory. Ruling: the success completes the step, the fumble still imposes its complication. |
|
||
| `R-25` new specialisation | "(highest related − 0)" is unclear and far too generous. Start new specialisations at printed base. |
|
||
| `R-10` | Promises location consequences but supplies **no table**. Rolling a location then rolling BRP's Major Wound table can contradict itself (location "arm", result "facial scar, lose CHA"). Write the seven-location consequence table. |
|
||
| `DR-01` chase | Still no opposed Athletics/Pilot pursuit procedure. Expedition legs are not combat chases. |
|
||
| `R-08a` / `R-24d` | Zero-g and vacuum are asserted as "familiarities" with no effects, acquisition rules or schema. |
|
||
| `R-30` | Automatic-effect powers have labels but no rule for counter matching, precedence, or where immunity tags live. |
|
||
| §8 | Character familiarity state, species, pursuit, weapon, armour, defect and vehicle schemas still absent. Deferring the *data* to v0.3 is fine; deferring the *schemas* is a v0.2 blocker. |
|
||
|
||
### The four new subsystems
|
||
|
||
**`R-24` Tradition and familiarity — right mechanic, wrong granularity.**
|
||
Numbers endorsed: keep 0 / −20 / −50 / locked (at skill 60 that reads 60% / 40% / 10%, a legible
|
||
spread). But analogy and unlocking belong to a **function**, not a whole device — a Builder
|
||
console can be analogous for *operate*, alien for *diagnose*, impossible for *repair*. Also, one
|
||
tradition per device cannot model hybrid Known Space tech. Restructure to
|
||
`functions.{operate,diagnose,repair}: {analogueIn, requiredDiscoveryIds}` with `traditionIds` as
|
||
an array.
|
||
|
||
**`R-25` Research — generates play, calendar-heavy but not table-heavy, internally inconsistent.**
|
||
Codex's timing analysis: a four-step solo project at skill 50 averages ~1,600 hours (160
|
||
ten-hour days); three researchers at 50% give an 87.5% per-step success chance, cutting it to
|
||
~92 days. Defensible if it is downtime. Needs: per-step `difficulty` (default Average, currently
|
||
referenced but never stored), a cap on participating rollers, stated objectives for field steps
|
||
(`resolveInPlay: true` is not an objective), and five as the maximum *initial* length with
|
||
fumble-inserted setback steps permitted beyond it.
|
||
|
||
**`R-31` Expedition — light enough, but costs and skill roles are broken.**
|
||
- "−1 supply, −1 power; vehicle legs −1 power instead of −1 supply" is ambiguous. Ruling: foot leg
|
||
−1 Supply; vehicle leg −1 Supply **and** −1 Power. People do not stop eating inside vehicles.
|
||
- Allowing Pilot *instead of* Navigate lets a good driver find a rumoured destination without
|
||
navigating. Navigate governs every overland leg; Pilot mitigates hazards or a failed Navigate.
|
||
- Failure currently lets the party take extra time whenever time is cheap, dodging resource
|
||
pressure. Make failure cost baseline **plus one resource** and +50% elapsed time.
|
||
- Fumble: pay baseline, no progress, −1 Integrity, hazard.
|
||
- Critical should find a Site only on an **unexplored** leg, or players farm Sites on repeat runs.
|
||
- Recovery has no numbers: one day, one roll — success +2, special +3, critical +4.
|
||
- One Integrity track cannot describe several vehicles. Either name it abstract or track per asset.
|
||
|
||
**`R-32` First contact — generates a scene, but READ is weak and Standing moves far too fast.**
|
||
The maths: at skill 60, only a special-or-better READ reveals the fact that makes OFFER Easy —
|
||
**12% of attempts**. The other 48 points of success merely report disposition. That contradicts
|
||
the claim that READ is the highest-value action. Fix: success reveals one taboo *or* desire;
|
||
special reveals both; critical reveals both plus +20% to a later stage. Remove Standing changes
|
||
from READ — silently understanding someone should not make them like you. Cap permanent Standing
|
||
change at **±1 per encounter**; +3 should need at least three contacts. And OFFER needs
|
||
communication gating: if SIGNAL failed with no shared language, verbal Bargain is unavailable,
|
||
though a nonverbal gift or ritual may be attempted at Difficult (Easy if READ found the form).
|
||
|
||
### §0.3 — the hit-points pushback
|
||
|
||
**Accepted, with a caveat that matters.** Codex agrees HP must halve because the location pools
|
||
are gone, and that keeping ~26 HP with no locations would remove the danger. But it rejects the
|
||
framing that this is an *equivalent* trade:
|
||
|
||
> At HP 13 / Major Wound 7, a 1D20+5 variable sword causes a Major Wound on **95%** of hits and
|
||
> deals 13+ damage on **65%**. Against the original 26 general HP its base damage could never
|
||
> empty the pool in one hit, though it would devastate a location. v0.2 therefore converts
|
||
> frequent mutilation into frequent total incapacitation.
|
||
|
||
Ruling: keep the formula, do not sign off the combat balance until signature weapon damage,
|
||
armour values and the missing location-consequence table are tested together.
|
||
|
||
### Foundry schema failures (12 found)
|
||
Highlights: no character familiarity state anywhere; device modelling too coarse for
|
||
function-level analogy; Discovery has two competing authorities (`unlocks` is function-specific,
|
||
`scope` is tradition-wide) with no canonical matching rule; `holders` contradicts party
|
||
ownership and raw Actor UUID arrays break on import/duplicate; numeric `base` cannot express
|
||
`DEX×2` or `EDU×5`; projects omit difficulty, remaining hours, researcher caps and setback
|
||
links; the scrith material entry silently drops `deep_radar`, `hyperwave` and neutrino
|
||
absorption; `ALL` has no tag semantics; no weapon schema exists although materials expect
|
||
penetration tags; expedition data lacks leg records, explored status and multiple assets;
|
||
combat defence state has no defined owner document.
|
||
|
||
### Highest-value change for v0.3
|
||
|
||
> Make v0.3 a **complete vertical-slice playtest**, not a broad data release: one four-leg
|
||
> expedition with exact costs and hazards, leading to one native first contact, one three-step
|
||
> field research project, and one Builder device with function-level familiarity and Discovery
|
||
> data. Run that chain end to end **without GM invention or hand-edited Foundry state.** Fix
|
||
> every rule and schema that blocks it.
|
||
|
||
> "That single slice will test all four new subsystems — and reveal more than fifty pages of
|
||
> untested equipment data."
|
||
|
||
### Verification note
|
||
Ten of Codex's checkable factual claims have now been re-verified against the source corpus
|
||
across two rounds. **All ten held.** The only claim that initially failed to verify — library
|
||
research at 100 hours — turned out to be truncated by Claude's scratch deduplication script;
|
||
the un-deduped extraction confirms it.
|
||
|
||
---
|
||
|
||
## Round 3 — Codex review of RINGBRP_RULES_SPEC_v0.3.md
|
||
**Date:** 7 August 2026 · **Reviewer:** Codex (GPT-5.4) · **Author:** Claude (Opus 5)
|
||
|
||
### Arithmetic audit — Claude's published table was wrong in four places
|
||
|
||
Claude independently recomputed every P(Major Wound) with an exact-enumeration script
|
||
(`scratchpad/mw.py`) rather than trusting either party. Codex's recomputation and Claude's agree
|
||
exactly. **The v0.3 §3.4 table contained four errors.**
|
||
|
||
| Weapon | v0.3 published | Correct (MW 9) |
|
||
|---|---|---|
|
||
| Club / baseball bat | ~25% | **33.3%** |
|
||
| One-handed sword | ~30% | **37.5%** |
|
||
| Crossbow | ~25% | **18.8%** |
|
||
| Two-handed sword | ~60% | **76.0%** |
|
||
| Medium pistol | ~20% | **25.0%** |
|
||
|
||
Confirmed correct: punch 0%, dagger 8.3%, heavy pistol 40.0%, heavy rifle 58.3%, shotgun close
|
||
74.1%, hand beamer 50.0%, variable sword 85.0%, laser pistol/rifle 100%.
|
||
|
||
**The design conclusion survives** — the ladder still reads correctly from fists to lasers — but
|
||
the table must be republished with exact figures.
|
||
|
||
### Major Wound threshold — floor it at 9
|
||
|
||
Codex: `ceil(HP/3)` gives a light human (HP 23) a threshold of 8, and the step from 9 to 8
|
||
**triples** the dagger result (8.3% → 25.0%) and doubles several others. Verified independently.
|
||
Lower HP already makes small characters easier to drop; the extra wound cliff is unearned.
|
||
|
||
**Adopted: `MAJOR_WOUND = max(9, ceil(HP/3))`.**
|
||
|
||
Also: Claude's claim that 9 is "almost exactly BRP's absolute number" is **false** — BRP's is ≈7.
|
||
Nine is justified because it produces the right weapon ladder, not by BRP parity. Rationale text
|
||
must be corrected.
|
||
|
||
### Marksmanship Style — two dead rules and a trap
|
||
|
||
1. **"Reflex: first Dodge ignores the multiple-defence penalty" is a no-op.** The first defence in
|
||
a round never carries that penalty. Dead rule. If a benefit is intended, it must be *"the first
|
||
Dodge does not increment `defencesThisRound`."*
|
||
2. **"Deliberate: may aim across consecutive rounds, maximum +30%" is a no-op.** Deliberate
|
||
already reaches +30% from a single round of aiming. Dead rule.
|
||
3. **Gravity does not actually drive the choice.** Heavy-gravity characters roll DEX 2D6+6 *and*
|
||
POW 2D6+6 — both average 13. There is no mechanical pull toward Deliberate; the link is pure
|
||
flavour. Codex: make gravity **determine** the governing characteristic (light = DEX,
|
||
heavy = POW, normal = choice) rather than merely suggest it.
|
||
4. **Deliberate becomes a trap at high skill.** Codex modelled both styles at weapon 60 and found
|
||
a genuine trade (Reflex +31 moving/short; Deliberate +9 long, +7 aimed). But around weapon 90
|
||
the bonuses hit the 95% success ceiling, so Deliberate's advantages stop paying while its −20%
|
||
move-and-fire penalty persists — and style is permanent. Fix: give Deliberate something that
|
||
survives the ceiling, e.g. *a full round of aim treats long range as medium*, or *an aimed hit
|
||
improves one success level*.
|
||
|
||
### Major Wound consequence table — not ready
|
||
|
||
- Results 1–2 make **20% of all Major Wounds** merely "lose one action". Result 3 is nearly the
|
||
same. Too much of the table is nothing.
|
||
- Results 4–5 and 8 have the attacker choose arm or leg — that **is** location consequence, so
|
||
the "effects, not anatomy" claim is false. Assign 4 to manipulation impairment and 5 to
|
||
mobility impairment; remove the choice.
|
||
- "Hand unusable" and "two-handed actions at −30%" contradict each other — two-handed use should
|
||
be impossible.
|
||
- Disabled leg halves MOV but says nothing about Dodge, Athletics, falling or standing.
|
||
- Result 7 never says which skills, who picks the sense, or the treatment difficulty and time.
|
||
- Result 8's "permanent if untreated" conflicts with the Luck roll, which only governs
|
||
characteristic loss.
|
||
- Result 9 makes the GM invent the characteristic and never defines "out of the fight".
|
||
- **Repeated Stamina rolls have no stopping condition.** At CON 13 a character has only
|
||
`0.65³ = 27.5%` chance of still being conscious after three checks. Successful First Aid must
|
||
stop the sequence.
|
||
- §3.2 says the table fires "at most once per fight"; §3.5 says it fires on every Major Wound.
|
||
Contradiction with no state to back either.
|
||
- "Permanent" characteristic loss contradicts the later statement that an autodoc reverses it.
|
||
- Variable sword shifting only 8–10 → 10 yields amputation on ~28.5% of hits. If the intent is
|
||
the source weapon's mutilation rate, shift **4–10 → Catastrophic** (~66.5% of hits).
|
||
|
||
### Round-2 carry-over: 20 closed, 11 reopened
|
||
|
||
**Closed:** simweb trigger · library hours · mixed research results · new specialisation base ·
|
||
step difficulty · step cap · field objectives · maxResearchers · leg cost · Navigate vs Pilot ·
|
||
critical-on-unexplored · READ · Standing cap · OFFER gating · function-level analogy · holders ·
|
||
experienceEligible · sensor properties · `ALL` removal · chase procedure.
|
||
|
||
**Reopened / partial:** gravity enforcement and high-skill balance (`R-12b–d`) · Major Wound table
|
||
internals (`R-10`) · which resource failure consumes and who chooses (`R-31`) · recovery-fumble
|
||
target (§5 and §8 disagree) · two competing Integrity authorities · `unlocks` cannot resolve
|
||
because devices have no `deviceId` · `skill.base` formula grammar undefined · weapon schema
|
||
exists but is not buildable · **vacuum still has no effects or acquisition rule** · counter vs
|
||
immunity conflated with unrepresented precedence · species/pursuit/equipment/defect/vehicle
|
||
schemas **falsely marked closed — still absent**.
|
||
|
||
### Schema failures (worst)
|
||
No `deviceId`, so `device.cziltang_brone.operate` is a dangling reference · raw unlock paths need
|
||
string parsing and are not import-safe (use `{documentId, functionId}`) · derived HP and MW are
|
||
stored with no recalculation rule and will drift from CON/MAS · `activeWounds` can encode only
|
||
Bleeding · `rateOfFire: "1/3"` is an uninterpreted string with no round semantics, ammo cost or
|
||
reload · `shots` conflates capacity with current ammunition · shotgun has two competing damage
|
||
authorities · weapon `traditionId` singular vs device `traditionIds` array · armour lacks equipped
|
||
state, degradation and damage-type interaction · material `immuneTo` and weapon `penetrationTags`
|
||
are different namespaces with no matching rule · device functions lack governing skill, difficulty,
|
||
effect, duration and power cost · project steps are not a discriminated union · expedition legs
|
||
lack difficulty, duration, cost and table references · these are `system` fragments, not Foundry
|
||
documents — no `name`, `type`, `_id`, schema version, migrations or validation.
|
||
|
||
### Vertical slice — not buildable
|
||
|
||
> "No. The slice in §6 is a requirements list, not runnable content."
|
||
|
||
Missing: four actual leg records · the six-entry site and hazard tables · which resource a failure
|
||
consumes · Hanging People group statistics · their true taboo, desire, correct offer form and
|
||
rewards at Standing +1/+2/+3 · complete READ/SIGNAL/OFFER procedure (v0.3 has only amendment
|
||
summaries) · three complete research-step records · the brone's per-function skills, difficulties,
|
||
effects and power costs · Machine People raider statistics, ammunition, tactics and encounter
|
||
geometry · pregenerated explorers · firearm rate-of-fire and reload rules · executable wound and
|
||
treatment rules.
|
||
|
||
**Document-level contradiction:** the scope line says unchanged rules "carry over" from v0.2, but
|
||
the acceptance test permits *only this document*. A playtest spec must either contain the complete
|
||
procedures or formally import a versioned rules core.
|
||
|
||
### Verification note
|
||
Claude verified the arithmetic independently rather than accepting it. Codex's recomputation was
|
||
correct and Claude's published table was not. Across three rounds, every checkable Codex claim
|
||
that has been tested has held.
|
||
|
||
---
|
||
|
||
## Round 4 — Codex acceptance test of RINGBRP_v0.4_PLAYTEST_SLICE.md
|
||
**Date:** 8 August 2026 · **Reviewer:** Codex (GPT-5.4), running §15 as a GM · **Author:** Claude (Opus 5)
|
||
|
||
### Verdict
|
||
|
||
> **"FAIL. v0.4 is not runnable with zero GM invention."**
|
||
|
||
This is the correct and useful outcome of a first acceptance test. The rules survived three rounds
|
||
of review; they did not survive contact with actual content. That is what the slice was for.
|
||
|
||
Codex played the scenario start to finish, logging every point where it had to invent. It also ran
|
||
a 100,000-fight Monte Carlo model of the combat encounter.
|
||
|
||
### The three hard blockers
|
||
|
||
**1. The scenario gate is mathematically shut.** The control plate — the object the entire research
|
||
chain depends on — is a Standing **+3** reward (§11.4). But permanent Standing is capped at **±1
|
||
per encounter** (§6, a cap adopted from Codex's own round-3 advice), and the slice contains exactly
|
||
one contact. **+3 is unreachable.** Falling back to the random Site table, the plate requires a
|
||
critical Navigate on an unexplored leg followed by a 2 on 1D6:
|
||
|
||
> `1 − (1 − 0.02/6)² = ` **0.666%**
|
||
|
||
Two rules that were each individually correct combined into a scenario that cannot be completed.
|
||
|
||
**2. Combat cannot be resolved consistently.** There are **no general range modifiers anywhere in
|
||
the system** — so a pistol at 60 m fires at its full rating, directly contradicting §12.3's "their
|
||
weapons are useless at 60 m". Also missing: the hand beamer's range bands, the raiders' gravity
|
||
and marksmanship styles, movement distance while firing, DEX-tie resolution, a definition of
|
||
"down" for the morale trigger, and a minimum of zero on post-armour damage.
|
||
|
||
**3. The device is unusable even on success.** The brone is stored at Integrity 0. Research
|
||
unlocks `operate` but nothing repairs it — unlocking and repairing are different states with no
|
||
transition. Worse: complying with the raiders (an outcome §12.3 explicitly endorses) costs 3
|
||
Power, leaving 3, and `operate` costs 4.
|
||
|
||
### §12.4's Major Wound claim is false
|
||
|
||
The slice advertises that the heavy rifle exercises the Major Wound table at **58.3%**. Against the
|
||
party's actual armour:
|
||
|
||
| Target | P(Major Wound) per hit |
|
||
|---|---:|
|
||
| Unarmoured, MW 9 (the quoted figure) | 58.3% |
|
||
| Dunn / Tomas, armour 4 | **8.3%** |
|
||
| Ceyla / Rakhi, armour 6 | **0%** |
|
||
|
||
Fix: give the two rifle raiders Deliberate style and have them alternate aim and fire; their aimed
|
||
shots ignore 4 points of armour and would genuinely exercise the table.
|
||
|
||
### Combat model — neither style dominant, and Claude's suspicion confirmed
|
||
|
||
Sustained party output: Reflex pair 5.04 HP/round at entry, 6.44 at short range; Deliberate pair
|
||
6.22 HP/round. Deliberate is marginally better at entry, Reflex at close quarters.
|
||
|
||
> "Neither style is indispensable... If the beamer is ruled out of range, Reflex becomes clearly
|
||
> dominant. **The missing beamer range controls the conclusion.**"
|
||
|
||
Claude had independently flagged that the geometry might not test the styles as designed. Confirmed:
|
||
the pneumorifle's 200/400/600 bands mean the 60 m engagement is *short* range, where Deliberate's
|
||
+10% long-range bonus never triggers.
|
||
|
||
Monte Carlo (100,000 fights): party forces morale in 99.3–99.96% of runs, but casualty rate swings
|
||
from **14.5% to 81.2%** purely on whether the raiders focus fire or spread — an unstated NPC ruling,
|
||
not player tactics.
|
||
|
||
### Claude's three self-found defects — all confirmed by Codex
|
||
|
||
| Defect | Codex |
|
||
|---|---|
|
||
| §13.2 applies the facility modifier after the difficulty multiplier, contradicting §1 | Confirmed `[CONTRA]`. §1 gives 26%/20%, the worked example gives 16%/10% |
|
||
| §14 familiarity example says "5%" then "1%" | Confirmed `[CONTRA]` — "calls the result effective 5%, then says the floor applies, then says the floor does not apply, finally resolving it at 1%" |
|
||
| Physical category governed by STR, not DEX | Not raised by Codex, but stands — it makes Dodge, Athletics and Stealth strength-based, penalising the light-gravity pilot by 4 points |
|
||
|
||
### Pregens — verified sound
|
||
|
||
Codex independently recomputed every derived value for all four explorers. **All correct**, matching
|
||
Claude's own audit exactly. Skills judged "face-plausible" for the stated roles. Two caveats:
|
||
legality cannot be audited without an age/pursuit allocation procedure and a printed skill-base
|
||
catalogue; and Tomas carries a "minidoc" which §3.5's autodoc list (deskdoc/shipdoc/megadoc) does
|
||
not define.
|
||
|
||
### Research timing
|
||
|
||
Claude's 24.4% combined figure is arithmetically right *under the published (wrong) modifier order*,
|
||
but the calendar claim is not: expected **82 days**, not 60–80. Under §1's correct order it is
|
||
**49 days**. The cracked-plate penalty is catastrophic — a further −20 under difficulty-first
|
||
ordering drives both researchers to ~1%, giving **~1,005 expected research days**.
|
||
|
||
> "This is dead time, not play: identical rolls occur after 16–20 day gaps with no intervening
|
||
> decisions, threats or resource pressure."
|
||
|
||
Fix: replace the cracked penalty with a fixed 100-hour Average Repair step; make the 200 hours a
|
||
one-time investment with failed retries costing 40 hours and granting +10% cumulative calibration.
|
||
|
||
### Other confirmed contradictions
|
||
|
||
- **§1 fumble ordering.** The fumble test runs before the "96–99 always fails" clause, so at
|
||
effective values up to 80 some of 96–99 are fumbles rather than failures. Rewrite as: *"96–99
|
||
cannot be an ordinary success; they are failures unless the fumble range includes them."*
|
||
- **§5** Pilot may re-roll a failed Navigate "once per leg" without restricting it to vehicle legs —
|
||
Dunn can use Pilot (Flycycle) while walking.
|
||
- **§5** recovery fumble must damage a *different* track, but rolls 4–6 damage Integrity even when
|
||
Integrity was being repaired.
|
||
- **§§10, 11.4** contact happens *after* leg 3, so the leg-3 rewards are retroactive.
|
||
- Ceyla has no Athletics rating despite hazards and the aperture requiring it. Suggested 32%.
|
||
- Site 3 (Ghoul waystation) is a legal random result with no statistics, taboo, desire or rewards.
|
||
|
||
### Round-3 closure audit
|
||
17 of 22 closed. **Reopened:** result-8 permanence vs the universal Luck roll · full Foundry
|
||
documents ("incomplete JSONC specimens, not a creatable slice"). **Partial:** result-9 mapping for
|
||
multi-tag weapons · simultaneous wound sequences · the `majorWoundThisCombat` flag has no
|
||
mechanical purpose or reset · finite beam cells contradict "no reload".
|
||
|
||
### Required for v0.5
|
||
1. Move contact **before** the leg-3 roll; make immediate rewards depend on *impression* within the
|
||
encounter while permanent Standing still moves only ±1.
|
||
2. Add a general range modifier ladder: short +0, medium −10%, long −30%, extreme −50%, beyond
|
||
impossible. Retain the style +10%s. Movement with an offensive action = half MOV (15 m).
|
||
3. Give every weapon range bands in its own gear entry.
|
||
4. Give NPCs gravity origin and marksmanship style.
|
||
5. Make research completion set the brone's repair to Average at −20 familiarity; operation failure
|
||
costs 1 Power, not 4.
|
||
6. Fix the §1 modifier-order authority and apply it consistently to every worked example.
|
||
7. Physical category → DEX.
|
||
|
||
---
|
||
|
||
## Round 5 — Codex acceptance test of v0.5 · **FAIL**
|
||
**8 August 2026.** 32 of 47 closed · 14 partial · 1 open · **10 newly broken**.
|
||
|
||
**The decisive finding: v0.5's reachability claim was arithmetically false.**
|
||
Codex computed the informed contact route: READ 58 (special-or-better 12%), climb 48%,
|
||
best SIGNAL 29%, OFFER Easy 78%. Result: **P(Impression +2) = 1.697%, P(+3) = 0.174%.**
|
||
Modal outcome +1 at 55.1%. The random Site-2 fallback gave 0.499% (0.998% with a guide).
|
||
**The control plate remained gated.** Two versions had now failed on the same thing by
|
||
different routes.
|
||
|
||
Other confirmed defects: 30-item acceptance log including undefined critical/special attack
|
||
effects, incomplete action economy, cover that both penalised and blocked, pistol positions
|
||
contradicting the bounds route, no rifle aim cadence (without which the Major Wound table never
|
||
fires against armour 6), incomplete targeting ties, unpublished printed bases (so §1's 5% floor
|
||
could not be applied — and repair's true floor is 5%, not the 1% published), unpublished item
|
||
masses (so the eye-storm hazard could not resolve), and the post-compliance Power budget leaving
|
||
the brone inoperable after a diagnose.
|
||
|
||
Corrected maths Codex supplied and Claude accepted: research **93.5% by day 32, expected 25.9
|
||
days** (not "20–32"); step 3 expected 1.65 attempts; repair is a **reflecting walk to net
|
||
Integrity 3**, expected **8.48 attempts / 33.9 hours** (not "three successes, eight attempts").
|
||
|
||
Codex's combat simulation (20,000 fights/case): in cover shooting the rifles first — 50% chance
|
||
someone goes down, 0.2% TPK. Out of cover — 88%. Shooting the nearest raider instead of the
|
||
rifles — **48% TPK**. The tactical difference is enormous and was undocumented.
|
||
|
||
---
|
||
|
||
## Round 6 — v0.6 submitted
|
||
**8 August 2026.**
|
||
|
||
### The structural fix — `R-37`
|
||
> **A scenario-critical object must never sit behind a probability gate.**
|
||
> Rewards accelerate; they never gate.
|
||
|
||
The dead flyer is now a **fixed location**. Contact determines only speed and cost: +2/+3
|
||
immediate escort, +1 one-day detour, 0 three-day detour and −1 Supply. Contact keeps real weight
|
||
through a new reward — researching as the band's guests costs **no Supply**, against ~9 Supply
|
||
otherwise across a 45-day project.
|
||
|
||
### Also in v0.6
|
||
`R-38` project-hours not researcher-hours · `R-39` critical ignores armour, special halves it ·
|
||
`R-40` complete action economy (movement is free; cover, reload, aim and First Aid each consume
|
||
the action) · `R-41` cover penalises without blocking · `R-42` the immediate Stamina roll is the
|
||
first of three; unconsciousness 1D6×10 min · `R-43` targeting: nearest → lowest HP → lowest DEX,
|
||
and the down are not legal targets · `R-44` one OFFER per encounter · `R-45` margin defined ·
|
||
`R-46` climb failure · `R-47` diagnose costs 0 Power after the Discovery and reveals the exterior ·
|
||
`R-48` fumble Power cost · `R-49` net Integrity 3 · `R-50` abstract rations · `R-51`–`R-52` Ghoul
|
||
route completed · `R-53` complication 6 statted · `R-54`–`R-55` stationary Supply and hazards ·
|
||
printed bases and item masses published.
|
||
|
||
### Implementation
|
||
The ruleset now exists as a **working Foundry VTT system** at
|
||
`~/FoundryVTT/Data/systems/ringbrp` (v0.6.0), verified in a live Foundry v14 instance with zero
|
||
load warnings. Rules verified in-engine against the spec: fumble bands 98 at skill 60 and 96 at
|
||
20; 96–99 never succeed and 00 always fumbles; difficulty-before-situational (72 Difficult −20 =
|
||
16); HP and the floored Major Wound; the range ladder; critical/special/full armour; repair 5%
|
||
pre-Discovery and 38% post; diagnose 1%; aimed rifle 70% at 60 m.
|
||
|
||
---
|
||
|
||
## Round 6 — Codex review of v0.6 · **FAIL**, but the gate is closed
|
||
**8 August 2026.** Of the 30 round-5 acceptance items: **21 closed, 6 partial, 3 open.**
|
||
|
||
### R-37 worked — and then the same bug appeared twice more
|
||
|
||
> "R-37 closes the control-plate discovery gate. It does not remove every single point of failure
|
||
> from the critical path."
|
||
|
||
**1. The eye-storm could permanently delete the cziltang brone.** Rakhi's heaviest carried item
|
||
*is* the brone, at 12.5 kg — exactly what the hazard takes. Codex's arithmetic: 19.25% per
|
||
stationary hazard check, **57.5% cumulative across four**. The scenario would have died on a dice
|
||
roll more than half the time during the research phase.
|
||
|
||
**2. Power could be exhausted to a dead end.** The rim-wall site was not declared a Power-recovery
|
||
installation, and each failed operation costs 1 Power at a 48% failure rate.
|
||
|
||
**3. The "exactly enough, once" Power budget was false.** The now-guaranteed flyer yields 1D3
|
||
Power, so the post-compliance budget is **5–7**, not 4.
|
||
|
||
**4. The impression table was still wrong.** SIGNAL special (+1) plus OFFER special (+1) is **+2**,
|
||
not +3. Third time that arithmetic has been published incorrectly.
|
||
|
||
### Code-vs-spec divergences found (14)
|
||
Stale RollTable wording · pistol route contradicting the journal · Dodge base 0 vs DEX×2 · Ghoul
|
||
weapon mismatch · missing configured expedition/project/discovery · device fields absent from the
|
||
schema · rifle cadence and corrected pistol positions stranded in the Foundry journal and never
|
||
moved into the written spec.
|
||
|
||
---
|
||
|
||
## v0.6.1 — the response
|
||
|
||
`R-56` **THE CRITICAL-PATH INVARIANT**, promoted from an ad-hoc fix to a standing rule:
|
||
|
||
> *Nothing on the critical path may be destroyed, exhausted or made unreachable by a random
|
||
> result.*
|
||
|
||
- Hazards never take mission-critical equipment. Rakhi loses the toolkit, not the brone. Non-critical
|
||
losses are recoverable on a Spot the next day.
|
||
- The rim-wall site **is** a Power-recovery installation. Running dry costs days and Supply, never
|
||
the scenario.
|
||
- Power budget corrected to 5–7.
|
||
|
||
Plus `R-57` cell accounting · `R-58` the impression matrix, correct at last (+3 requires a
|
||
critical) · `R-59` rifle cadence and pistol positions moved into the spec · `R-60` the −3 archer
|
||
branch given a real procedure instead of a scope exemption · `R-61` scavenger theft procedure.
|
||
|
||
**Foundry synced to v0.6.1** and verified live: 9 compendia including a fully configured expedition
|
||
Actor carrying the three-step project, the discovery with its structural unlock reference, and the
|
||
contact group with its taboo; Dodge base as a formula; hazard tables rewritten; Ghoul hook.
|
||
|
||
### The pattern worth keeping
|
||
Three versions failed on the same class of defect in three different disguises — a gate, then a
|
||
destructible object, then an exhaustible resource. The lesson is not any of the individual fixes.
|
||
It is that **a critical path needs an invariant, not a patch**, and that only playing the thing
|
||
end to end reveals which disguise it is wearing this time.
|
||
|
||
---
|
||
|
||
# Round 7 — Strike Rank (Option 1) implementation + statline audit
|
||
*8 Aug 2026. Brief: `reviews/BRIEF_r7_strikerank_statlines.md`. Raw: `reviews/r7_codex_review.md`.*
|
||
|
||
Owner chose **Option 1, Deterministic Strike Rank**. Built, then reviewed. Codex returned
|
||
seven Part A findings and six Part B findings. Every claim below was checked against the
|
||
running system before being accepted or rejected.
|
||
|
||
## Verified true and FIXED this round
|
||
|
||
`R-57` **The expedition sheet was printing the answers to the first-contact challenge.**
|
||
The contacts tab rendered every taboo, every desire and `correctOfferForm` verbatim off the
|
||
embedded group item, while the expedition's own `knownTaboos` / `knownDesires` were empty and
|
||
`pidginEstablished` was false. The sheet displayed *"A gift of 0.5 kg of salt, passed hand to
|
||
hand, with both parties off the ground"* — the exact solution — to anyone who could open it.
|
||
Now: players see only what the expedition has actually learned, plus "Nothing learned about
|
||
this group yet."; the GM sees everything, marked GM ONLY with a yellow rule.
|
||
**This is the most severe defect found in seven rounds** — it silently defeats a scenario
|
||
pillar rather than producing a wrong number. Verified from both the GM and the player path.
|
||
|
||
`R-58` **The PC-before-NPC tie-break never fired.** It tested `actor.hasPlayerOwner`, which is
|
||
false for every pregen in a world with no players yet assigned — so all four PCs were being
|
||
treated as NPCs. Now tests `actor.type === "character"`. Verified: Ceyla (DEX 12) correctly
|
||
precedes the DEX 12 raiders she ties with.
|
||
|
||
`R-59` **Simultaneous combatants sorted alphabetically.** Replaced with insertion order, so the
|
||
result is stable and reproducible rather than an artefact of naming.
|
||
|
||
`R-60` **Strike Rank went stale, so P-10 did not actually work.** Nothing recomputed initiative
|
||
when load, weapon, skill or DEX changed, so "carrying loot changes your action economy
|
||
mid-scene" was inert. Now recomputes on actor/item change. Verified live: dropping the 12.5 kg
|
||
cziltang brone takes Rakhi from **SR 8 to SR 6** unaided, and picking it up restores it.
|
||
|
||
`R-61` **Training modifier applied to actions with no governing skill.** Move, Ready and Operate
|
||
Device were each taking the −30% "Untrained" +1. T is now only applied where the action has a
|
||
governing skill.
|
||
|
||
`R-62` **Melee defaulted to the equipped ranged weapon.** Rakhi declaring melee was scored on her
|
||
Pneumorifle. The default weapon is now filtered to match the declared action.
|
||
|
||
## Verified true, NOT yet fixed — these need an owner decision or are a separate build
|
||
|
||
| # | Finding | Status |
|
||
|---|---|---|
|
||
| A1 | `E` uses `ceil` per `R-23` "each 10 kg **or part**". Tomas is **1.0 kg** over his free carry and pays a full step of MOV, skill *and* SR. Codex recommends `floor` — rejecting all four of my proposed fixes — which drops the PC mean E from 1.5 to 0.5 and removes the cliff, at the cost of departing from the 1984 rule. | **Owner decision.** Changes `R-23`, which also governs MOV and skill penalties, not just initiative. |
|
||
| A6 | `T` spread of −2…+1 equals the entire beam-to-slow weapon spread, so 79→80% skill can outweigh weapon choice. Codex recommends −1/0/+1. Neither 1984 nor BRP:UGE coupled skill to action order; it is wholly our addition. | **Owner decision.** Changes agreed Option 1. |
|
||
| A2 | **The declared action is not enforced.** SR is computed from the declaration, but the attack controls never check it and nothing marks the offensive action spent. A combatant can declare Reload, take the Reload ordering, and then fire anyway. SR is currently advisory. | Needs action-spent state. Separate build. |
|
||
| A3 | **Aiming is free.** The attack dialog offers an unrestricted "Aiming" checkbox that grants the bonus and armour bypass with no earned aim state. Confirmed at `ringbrp.mjs:1269`. | Interlocks with A2. |
|
||
| A4 | `reloadRounds` is duration being spent as ordering. A 3-round reload delays within the round instead of occupying three. There is also no reload resolution that transfers reserve ammunition. | Separate build. |
|
||
| B1 | The Discovery gate is not enforced, and cannot be until acquisition is representable: the expedition already **owns** the discovery item whose text says "Not yet earned", so presence ≠ acquired. `unlocks` and `requiredDiscoveryIds` are inverse copies that can disagree. | Needs an `earned` state + one canonical authority. |
|
||
| B3 | `material.immuneTo` has no invocation point — the damage button rolls a formula with no target and no material, so it cannot know scrith was struck. Also: spec v0.3 says `penetrationTags`, the code says `damageTags`. | Needs target-aware damage. |
|
||
| B4 | Device status is derived as functional only at full Integrity, ignoring per-function `requiresIntegrity`. Needs per-function `locked / damaged / unpowered / ready`, plus a `modifiedByDiscoveryId` distinct from hard prerequisites. | Carried from round 2. |
|
||
| B6 | `group` has no shape in spec v0.3 at all — it was invented for the Foundry build. Needs structured `offers[]`, reward IDs, negative-standing consequences, language gating for SIGNAL, and GM-only ownership of the definition. | Design work. |
|
||
|
||
## Codex's ranked enforcement cut line for the statlines
|
||
|
||
| Priority | Fields | Treatment |
|
||
|---|---|---|
|
||
| **P0** | `requiredDiscoveryIds`/`unlocks`, `immuneTo`, `materialId` | Hard gate or immunity, explicit GM override |
|
||
| **P1** | `skillBonus`, `powerDraw`, `blocksSensors` | Apply and show in the roll; debit resources |
|
||
| **P2** | `slowTo`, `neutrinoAbsorption` | Warn only — neither has a quantified meaning, so automating would invent a rule |
|
||
| **GM reference** | `correctOfferForm`, `rewardsByStanding` | Never auto-adjudicate prose; hide unrevealed, prompt the GM |
|
||
|
||
## Correction to our own published work
|
||
|
||
`INITIATIVE_OPTIONS.md` stated `E = floor(...)` and that Rakhi was the only encumbered
|
||
character. Both were wrong: the rule is `ceil`, and all four explorers are over their free
|
||
carry. The worked example has been replaced with the order the built system actually produces,
|
||
verified in Foundry 14.361. Codex also correctly noted the round-7 brief itself repeated the
|
||
stale `floor` — the brief was written from the doc, not the code.
|
||
|
||
**Scoreboard.** Round 7 is the first round where the most serious finding was an information
|
||
leak rather than an arithmetic error, and the first where a defect (`R-57`) would have been
|
||
invisible in play until it had already spoiled a session.
|
||
|
||
---
|
||
|
||
# Round 8 — encumbrance rework, readiness, dice, playthrough
|
||
*8 Aug 2026. Brief: `reviews/BRIEF_r8_encumbrance_readiness.md`. Codex round 8 was
|
||
still running at the time of writing; findings will be folded in when it returns.*
|
||
|
||
Owner directives implemented, then tested by play rather than by inspection.
|
||
|
||
`R-23` **revised.** Encumbrance now has two readings of one condition, deliberately:
|
||
`floor(over/10)` whole steps drive MOV and skill penalties as before, while a
|
||
continuous `ceil(over)/10` drives Strike Rank. Any overage at all now costs part of
|
||
a rank and no single kilogramme costs a whole one. The 1 kg cliff is gone.
|
||
|
||
`R-69` **Initiative is no longer only about weapons.** The single skill term split
|
||
into two, each narrow: **T** training, the declared action's own skill, and **R**
|
||
readiness, the best of Tactics, Insight, Spot and Listen, floored by INT × 3 so a
|
||
clever but untrained character is not flat. New **Tactics** skill, Mental, base 5.
|
||
|
||
`R-70` **The bands were tuned to a distribution that does not exist.** After
|
||
narrowing to −1/0/+1 the thresholds sat at 80 and 30 — but measured across all 102
|
||
skill ratings in the world, weapon skills span **31–59** and top out at 59, so the
|
||
−1 band was literally unreachable and both T and R were flat +0 for every character.
|
||
Retuned to **55 / 35**, the observed quartiles. Skill now actually moves initiative.
|
||
*Found by measuring, not by reading.*
|
||
|
||
`R-63`, `R-64` the two-axis modifier system and the ±40% two-modifier cap.
|
||
`R-65`–`R-68` defects found by playing: new characters started wounded, "Automatic"
|
||
difficulty rolled normally, aiming is free for rapid weapons, packs need a restart.
|
||
All detailed in `RULES_COVERAGE_AUDIT.md`.
|
||
|
||
**Dice.** Three house sets registered with Dice So Nice — Void (flat), Shadow
|
||
Squares and The Arch — in the sheet palette with Michroma numerals, plus matching
|
||
in-chat tens/units chips that work whether or not 3D dice are enabled.
|
||
One real integration bug: `addTexture` returns a **Promise**, so colorsets
|
||
registered in the same tick silently lost their textures. Every call is now awaited.
|
||
|
||
**Scoreboard.** Round 8's most valuable finding came from neither AI reviewing code
|
||
nor from the specs, but from measuring the actual data (`R-70`) and from creating a
|
||
character and playing it (`R-65`, `R-66`, `R-67`).
|
||
|
||
# Round 9 follow-up — blockers closed, findings 5/6 and R-56 fixed
|
||
*8 Aug 2026. Codex returned FAIL on round 9 with two blockers. Both were correct.*
|
||
|
||
`R-71` **The setting reproduced Fringeworthy's premise.** Under-ice gateway, ancient
|
||
builders, alternate Earths, and a rare compatible minority named for the fringe —
|
||
Codex was right that the combination could not read as coincidence, and that
|
||
"fringe-capable" was "Fringeworthy expanded into an adjective". Replaced with
|
||
Codex's own suggestion, which is a better idea than the original: **anyone can
|
||
cross, crossing is an induced misidentification, and Coherence is the accumulating
|
||
error.** No door, no builders, no network, no catalogue, no gifted minority. The
|
||
mechanic is now the reason travel works rather than a consequence of it.
|
||
|
||
`R-72` **Both species were reskins.** Triune retained the entire Puppeteer design
|
||
and Ferren was still recognisably a Kzin. Replaced from the silhouette up: the
|
||
**Vesh** is bilaterally asymmetric with no head at all (senses on a dorsal ridge,
|
||
so no killing headshot and no face to read), and the **Cadence** is four to six
|
||
bodies that are one person, so the location roll picks which body is hit and
|
||
killing one reduces rather than kills.
|
||
|
||
`R-73` **Displacement was prose only** — zero clamped and sat there, and recovery
|
||
would walk a displaced character back to 1. Now an event: resets to full, archives
|
||
the drift log, whispers the GM to change one true thing.
|
||
|
||
`R-74` **"−X% everything" affected almost nothing** — Coherence was applied in the
|
||
skill-tab handler only, so attacks, characteristic rolls, Luck and case phases all
|
||
bypassed it. Moved to the single choke point in `skillRoll`.
|
||
|
||
`R-75` **Coherence was entirely manual.** Now charged by the system: a fumble while
|
||
across, and any use of transposed materiel, both fire automatically; `debrief()` is
|
||
one operation and refuses to run while the team is across. Verified: 8 → 7 on a
|
||
fumble, 7 → 6 on the lance, blocked across, 6 → 7 at home.
|
||
|
||
`R-76` **The case-file remap was naming only.** The phase resolver still chose a
|
||
"navigator" by Navigate; it now leads on **Tradecraft** with Navigate as fallback.
|
||
`RINGBRP.caseTracks` maps the stored fields to case vocabulary — supply→Resources,
|
||
power→Clearance, integrity→Containment — without breaking existing actors.
|
||
|
||
`R-77` **R-56 violation confirmed and closed.** "Cannot open a path home unaided"
|
||
was a gate whenever no second agent existed. The way home is now never closed:
|
||
aided 0%, alone −30%, and a failed return lands you *near* the target rather than
|
||
nowhere. Verified: `possible=true` in both cases.
|
||
|
||
# Playthrough — The Ilfracombe Crossing, 8 Aug 2026
|
||
*Ran a whole case in the live world: import, four phases, a firefight, the device,
|
||
the way home, debrief. Nine defects, every one found by playing rather than reading.*
|
||
|
||
`R-78` **The case file shipped with no team.** `members: []`, so the phase resolver
|
||
had nobody to lead and Debrief iterated an empty list. The published case was
|
||
unusable on import. Now built with its four agents.
|
||
|
||
`R-79` **Two consumers, two shapes.** The resolver read `members` as bare ids; the
|
||
Debrief handler I wrote read them as objects. Standardised on ids.
|
||
|
||
`R-80` **Bulk find/replace corrupted the language file.** The setting rename ran
|
||
`" leg" → " phase"` and turned hit locations into **"Right phase"**, `LocEffect.leg`
|
||
into "The phase gives way", and a chat title into "case team phase". Seven strings
|
||
repaired. The lesson is the one from the earlier case-sensitivity miss: a rename
|
||
sweep needs an audit pass with *different* patterns from the ones that made it.
|
||
|
||
`R-81` **Two of the three case tracks were inert.** Across a full four-phase case,
|
||
Clearance and Containment never moved once — only Resources was driven by anything.
|
||
Failure now costs Containment, a fumble costs Containment 2 and Clearance 1.
|
||
|
||
`R-82` **R-56 violated at case scale.** An exhausted track set `inCrisis` and the
|
||
sheet then *blocked* phase resolution — a case that ran out of Resources could not
|
||
continue at all. Now it degrades: phases go Difficult and cost Containment, and can
|
||
always be attempted. Verified with Resources 0 and Clearance 0: phase resolved,
|
||
`blocked=false`.
|
||
|
||
`R-83` **Forced encounters were data nobody read.** Both dramatic beats of the case
|
||
— the contact and the ambush — were declared in the phase data and referenced by
|
||
zero lines of code. Now posted to chat when the phase resolves.
|
||
|
||
`R-84` **The phase lead took the best of Tradecraft or Navigate** rather than
|
||
preferring Tradecraft, so Dunn's Navigate 67 beat the Tradecraft it was meant to
|
||
showcase. Now Tradecraft first, Navigate only as fallback.
|
||
|
||
`R-85` **The device gate was never enforced** — Codex flagged this in rounds 7 and
|
||
9 and it was still true. `requiredDiscoveryIds` was read by no code, so the locked
|
||
function at the centre of the case could be operated on turn one. Now enforced with
|
||
four distinct refusals: locked, damaged, unpowered, ready.
|
||
|
||
`R-86` **Presence could not mean acquired.** The discovery item shipped *on* the
|
||
case file, so "does the team have it" was true from the start. Discoveries now ship
|
||
`earned: false` and `earnDiscovery()` is the moment the case turns.
|
||
|
||
**Scoreboard.** Playing found nine defects in one session, including two that made
|
||
published content unusable (`R-78`, `R-85`) and one invariant violation (`R-82`)
|
||
that the two AI review rounds had both missed at case scale despite catching its
|
||
character-scale twin.
|
||
|
||
# Second playthrough — BRACKWATER, 8 Aug 2026
|
||
*A deliberately different case: a Hollow world, unexplored phases, the tables, first
|
||
contact, and the new species anatomies. Four more defects.*
|
||
|
||
`R-87` **First contact had no engine at all.** Standings, impression, taboos,
|
||
desires and `pidginEstablished` were all stored, the three-stage READ/SIGNAL/OFFER
|
||
procedure was written up in the slice — and **zero code** touched any of it. The
|
||
entire social pillar was prose over inert data.
|
||
|
||
Worse, it stranded `R-57`. That fix hides a group's secrets until the team has
|
||
*learned* them, reading from `knownTaboos` — and nothing in the system could ever
|
||
write to `knownTaboos`. Players would have seen "Nothing learned about this group
|
||
yet" for the whole campaign, permanently. A privacy fix and a missing feature that
|
||
were individually invisible and together made the content unusable.
|
||
|
||
Now implemented: three stages with their own skill pools, READ writing learned
|
||
taboos and desires back to the standing, SIGNAL establishing pidgin, OFFER the only
|
||
stage that moves standing, and `R-44` (one OFFER per encounter) enforced. Verified:
|
||
READ learned the taboo and the desire, impression reached 2, a second OFFER in the
|
||
same encounter was refused and allowed in the next — and the player view then
|
||
correctly showed exactly the one taboo and one desire the team had earned.
|
||
|
||
`R-88` **A re-attempted phase re-fires its forced encounter.** A fumbled phase does
|
||
not advance, so the same forced encounter posts again. Left as-is for now — noted
|
||
rather than fixed, because "the thing you failed to get past is still there" is
|
||
arguably correct.
|
||
|
||
`R-89` **Setting content survived in the group and slice prose.** The desire was
|
||
still "Salt. None in the canopy; the nearest seep is eleven days away", the site was
|
||
a "dead flyer", and one heading read "the The Quiet Neighbours" from an earlier
|
||
substitution. Four sweeps had passed because each used the patterns of the previous
|
||
rename rather than looking for what the *content* was still about.
|
||
|
||
`R-90` **Cadence anatomy verified in play**: 26 hit points derived into five bodies
|
||
of 6, all `unit` kind, no vital and no head, location rolls distributing evenly.
|
||
|
||
**Scoreboard.** Two playthroughs, thirteen defects. The pattern across both is the
|
||
same: the mechanics that were *reviewed* held up, and the ones that were only ever
|
||
*written down* did not exist. `R-85` and `R-87` are both cases where a document
|
||
described a system in detail and no code implemented any of it.
|
||
|
||
# Third playthrough — wounds, displacement, healing. 8 Aug 2026
|
||
|
||
`R-91` **Armour was skipped on one of the two damage paths.** `resolveHit` applied
|
||
armour; `hitLocationRoll` did not, so the same blow did more damage when it went
|
||
through the location roll — the path the sheet button uses. Now applies worn armour
|
||
plus any location armour, with the critical bypass preserved. Verified: 7 damage
|
||
against armour 4 gives 3 through on a success and 7 on a critical.
|
||
|
||
`R-92` **Healing existed nowhere and now exists.** Flagged in the coverage audit and
|
||
twice more in play without being fixed: a wounded agent could not recover by any
|
||
means the system offered. Implemented BRP-standard natural healing (1 per week),
|
||
First Aid (3, +20% with a kit, −1 on a fumble, once per wound), the field medic pack
|
||
(1 per 4 hours) and the trauma unit (1 per 2 hours). Location damage mends alongside
|
||
hit points. Characteristic loss is still not reversed, which is the standing rule.
|
||
|
||
Verified: Dunn at 5/23 → First Aid +3 → 14 days natural +2 → 12 hours trauma unit
|
||
+6 → 16/23.
|
||
|
||
`R-93` **CLOSED — location mending was silently dropping full heals.** In the same run,
|
||
a 2-point heal correctly mended `legR` from 3 to 1, but a subsequent 6-point heal
|
||
mended nothing at all despite 4 points of outstanding location damage. The mend loop
|
||
in `heal()` was correct; the write was not. **Foundry MERGES object updates**, so
|
||
`delete dmg[id]` on a cloned object leaves the stored value untouched — a location
|
||
mended to zero silently kept its damage, while a partial mend appeared to work
|
||
because it was a decrement rather than a deletion. My guess that
|
||
`prepareDerivedData` was rebuilding the object was wrong: that code only reads it.
|
||
|
||
Fixed by writing per-key paths and removing cleared locations with Foundry's `-=`
|
||
deletion form. Verified against the exact sequence that failed: 6 trauma-unit points
|
||
now clear `{legR:1, armL:3}` to `{}`, a full heal empties the object, and a targeted
|
||
mend clears the named location first.
|
||
|
||
Corrections to my own earlier reporting: `resolveLocationHit` *does* return
|
||
`majorWound`, which I initially read as a missing check — the flag was there and my
|
||
test harness simply did not print it.
|
||
|
||
**Scoreboard.** Three playthroughs, sixteen defects. Two subsystems described in
|
||
detail by the design documents turned out to have no implementation at all (the
|
||
device gate, first contact), and one core rule had never been written in either
|
||
place (healing).
|
||
|
||
# Fourth playthrough — the end-of-case loop. 8 Aug 2026
|
||
|
||
`R-94` **Experience was stored and never earned.** Every skill carried
|
||
`experienceTicked` and `experienceEligible`, the sheet drew the tick, and nothing in
|
||
the system ever set one — so the flags were decorative and no character could ever
|
||
improve. A successful roll now ticks its skill automatically, and `endOfCase()` runs
|
||
the BRP improvement roll on every ticked skill: roll over the current rating to gain
|
||
1D6, ticks clear either way. Verified in play — two skills ticked from actual rolls,
|
||
both rolled under and gained nothing, ticks cleared, and a second call correctly
|
||
does nothing.
|
||
|
||
This is the step that closes the episodic loop the owner asked for: a case ends, the
|
||
agents are a little better, and the ticks reset for the next one.
|
||
|
||
`R-95` **CLOSED — gear bonuses were prose.** The cover identity pack promised "+20% to Fast
|
||
Talk" and the medical kit "+20% to First Aid" in their descriptions, while
|
||
`skillBonus` shipped as `{skillFamilyId:"", value:0}` on every item and was read by
|
||
no code. Now populated at build time AND read: `gearBonusFor()` applies the best matching
|
||
carried item at the `skillRoll` choke point, so every path gets it. Only the single
|
||
best item applies — a pack full of medical kits does not make anyone a surgeon — and
|
||
dropped gear stops helping immediately.
|
||
|
||
`R-97` **The medical kit had two authorities for one number.** `firstAid()`
|
||
hardcoded +20% while the item itself carried `skillBonus: +20% first_aid`, so once
|
||
the reader existed the bonus would have applied twice. The hardcoded copy is gone
|
||
and the item is now the only source. Verified: First Aid 78% → 98%, with two kits
|
||
still 98%, and 78% with the kit dropped.
|
||
|
||
`R-96` **CLOSED — NPC targeting was prose only.** "Fires at the nearest visible
|
||
agent; ties go to the lowest current hit points" was written into three statblocks
|
||
and lived nowhere else, so every GM arbitrated it by hand and inconsistently.
|
||
|
||
`npcTarget()` now resolves it in the published order — nearest visible, then lowest
|
||
current hit points, then lowest DEX, then the GM chooses from what remains — and a
|
||
DOWN character is not a legal target while anything is still standing, because
|
||
these enemies are being paid rather than fanatical and shooting the unconscious
|
||
wastes a round. `declareNpcTarget()` whispers the choice and the reason to the GM,
|
||
so the table can see the rule was followed rather than fudged.
|
||
|
||
`npcShouldWithdraw()` implements the morale line from the same statblocks — "when
|
||
two of a team of six are down, the rest withdraw" — as a fraction, so it holds for
|
||
any group size.
|
||
|
||
Verified across all eight branches: nearest wins; a range tie falls to hit points;
|
||
a hit-point tie falls to DEX; a full tie reports GM choice and returns the tied set;
|
||
downed targets are skipped while others stand; the downed become legal only when
|
||
nobody stands; hidden tokens are ignored; an empty field returns no legal target.
|
||
Morale fires at 2 of 6 and not at 1 of 6.
|
||
|
||
**Scoreboard.** Four playthroughs, nineteen defects. Every one of the last three
|
||
sessions found at least one subsystem that was fully described in the design
|
||
documents and entirely absent from the code.
|
||
|
||
# R-98 — the three "unloadable" fields, root cause. 8 Aug 2026
|
||
|
||
Not a Foundry bug, not a LevelDB bug, not a data-model bug. **Foundry was never
|
||
being stopped.**
|
||
|
||
Every restart this session used `pgrep -f "main.js --dataPath=..."` to find and
|
||
kill the server. That pattern matched the *first* instance, which ran as a bare
|
||
`node main.js`. After the app was relaunched with `open -a`, it ran as the bundled
|
||
Electron binary with entirely different process arguments — so from that point on
|
||
`pgrep` matched **nothing**, `kill` was a no-op, and Foundry kept running.
|
||
|
||
The check for "did it stop" used the same pattern, so an empty result was read as
|
||
"stopped" when it actually meant "not found". A verification that can only confirm
|
||
what it is looking for is not a verification.
|
||
|
||
Everything else follows: Foundry held its original in-memory compendium for hours,
|
||
never re-read the rebuilt packs, and served a `MARKER` value that survived deleting
|
||
the entire packs directory — because the process that held it never died. The disk
|
||
was correct the whole time. So were the build script, the data model and the packs.
|
||
|
||
Two false diagnoses were published along the way and are recorded here so they are
|
||
not re-guessed: that `prepareDerivedData` was rebuilding `locationDamage` (it only
|
||
reads it), and that concurrent read-write LevelDB opens were corrupting the packs
|
||
(a genuine hazard, but not this).
|
||
|
||
Correct way to stop Foundry, whatever it was launched as — find it by the port it
|
||
is actually serving:
|
||
|
||
PID=$(lsof -nP -iTCP:30000 -sTCP:LISTEN -t | head -1)
|
||
kill -TERM $PID
|
||
# then confirm the PORT is free, not that a name pattern is absent
|
||
|
||
Verified after a genuine stop, clean rebuild and fresh start: marker gone, gear
|
||
bonuses present, case file members 4, discovery `earned: false`, and the full path
|
||
from compendium to actor to roll works — First Aid 78% → 98% with the kit.
|
||
|
||
# Fifth playthrough — the import path, now that packs actually load. 8 Aug 2026
|
||
|
||
`R-99` **The shipped case file resolved to nobody.** `R-78` stored `members` as raw
|
||
actor ids taken from the compendium. Foundry assigns fresh ids on import, so after
|
||
a normal import the case file reported four members and resolved **none** of them —
|
||
the phase resolver had no lead and Debrief iterated an empty list. The count looked
|
||
right, which is why it survived the earlier playthrough: I checked `members.length`
|
||
and never checked that the ids pointed at anything.
|
||
|
||
This was only findable once the pack-loading problem (`R-98`) was fixed, because
|
||
until then no import reflected the built content at all.
|
||
|
||
Fixed by storing **stable keys** rather than ids: every pregen is stamped
|
||
`flags.ringbrp.key`, the case file stores those keys, and `caseMembers()` resolves
|
||
key → flag → id → name, tolerating all three shapes `members` has had over the
|
||
project's life.
|
||
|
||
A second defect inside the fix: the flag was initially written before `...BASE()`,
|
||
which spreads an empty `flags: {}` and silently overwrote it. Order matters in
|
||
object spread and the symptom was an undefined flag with no error.
|
||
|
||
Verified from a clean import: members resolve to all four agents, four phases run
|
||
with all three tracks moving (a phase-4 fumble took Containment 2 and Clearance 1),
|
||
contact resolves, the device gate correctly reports locked, and debrief recovers
|
||
Coherence for the whole team.
|
||
|
||
**Scoreboard.** Five playthroughs, twenty defects. `R-99` is the third case of a
|
||
fix that made the *symptom* look correct while leaving the behaviour broken —
|
||
alongside `R-95` (data populated, no reader) and `R-57`/`R-87` (secrets hidden, no
|
||
way to learn them). Checking the number rather than the behaviour is now the
|
||
single most reliable way I have produced a false pass.
|
||
|
||
# Sixth playthrough — the sheets as UI. 8 Aug 2026
|
||
*Everything to date had been driven through the API. This drove the actual DOM.*
|
||
|
||
`R-100` **The Resolve Phase button was permanently disabled.** The case-file sheet
|
||
computed its own phase lead — resolving `members` by raw id and picking on Navigate
|
||
— duplicating logic that `resolveLegDialog` had already been fixed to do via
|
||
`caseMembers()` and Tradecraft. So the API path worked perfectly while the button a
|
||
GM would actually click was dead, and the sheet reported `navigator: NONE`,
|
||
`blocked: true`.
|
||
|
||
Two copies of one lookup, one of them fixed. The sheet now calls the shared
|
||
resolver. Verified by clicking: `Ceyla Marspont 55% (tradecraft)`, rollTarget 27,
|
||
button enabled, phase 0 → 1, two chat cards, tracks moved.
|
||
|
||
Fixing it exposed two dangling references to the removed local (`ctx.blocked` and
|
||
`ctx.rollTarget`), both of which threw at render — caught immediately because the
|
||
sheet failed to open, which is the useful kind of breakage.
|
||
|
||
## What the UI pass confirmed working
|
||
|
||
Character, NPC, case-file, weapon, gear and skill sheets all render with no console
|
||
errors. 45 rollable elements on the character sheet. Clicking a characteristic
|
||
opens the roll dialog and posts to chat. The Coherence buttons move the track
|
||
(8 → 7). Tab switching works across all six tabs. On the case file: Across/Home
|
||
toggles, the six track-adjust buttons work, and Resolve Phase now runs.
|
||
|
||
## Two false alarms, both mine
|
||
|
||
I reported tab switching as broken; it was fine — I had several sheets open and was
|
||
reading `.active` from the wrong one. I reported the NPC sheet as failing to render;
|
||
it renders fine, my harness mishandled the element reference.
|
||
|
||
**Scoreboard.** Six playthroughs, twenty-one defects. `R-100` is the fourth
|
||
duplicated-logic defect (`R-97` medical kit, `R-84` phase lead, `R-76` case tracks)
|
||
— every one the same shape: a rule implemented twice, fixed once. Worth a standing
|
||
rule of its own: when fixing a lookup, grep for every other place that does it.
|
||
|
||
# Seventh playthrough — the journal and the tables. 9 Aug 2026
|
||
|
||
`R-101` **Old-setting vocabulary survived in the journal page titles.** "The Journey
|
||
— four legs" and "Combat — the defile" — plus body text — came through every
|
||
previous sweep because those sweeps searched for *proper nouns* (Kelnok, cziltang,
|
||
scrith) and the surviving words were ordinary ones the conversion had quietly
|
||
retired: "legs" for phases, "defile" for the terrain. Renamed to "The Approach —
|
||
four phases" and "Combat — the cliff path".
|
||
|
||
A capitalised-variant miss inside the same fix: the first pass caught `defile` and
|
||
`four legs` but not `Defile` and `Four legs` at the start of a heading. That is the
|
||
same case-sensitivity error made earlier in the project, in the same file.
|
||
|
||
284 documents now clean against a pattern including the ordinary-word retirees.
|
||
|
||
`R-102` **OPEN — table effects are prose.** Drawing from the hazard table returns
|
||
its text and changes nothing: no track is debited, no roll is prompted, and there
|
||
is no table-effect function anywhere in the API. Confirmed by drawing a hazard and
|
||
watching Resources/Clearance/Containment sit unchanged at 6/6/9. Same shape as the
|
||
device gate and first contact before them — a mechanic fully specified in content
|
||
and implemented nowhere. **Not attempted; no code written.**
|
||
|
||
## Duplicated-lookup audit
|
||
|
||
Acting on the standing rule from `R-100`, I grepped for every remaining duplicate of
|
||
the lookups that have caused four defects. All clean: `system.members` is now only
|
||
read through `caseMembers()`, and coherence and gear bonuses are each applied
|
||
exactly once, at the `skillRoll` choke point.
|
||
|
||
**Scoreboard.** Seven playthroughs, twenty-two defects. The pattern holds: reviewing
|
||
finds errors in code that exists; playing finds code that does not exist at all.
|
||
|
||
# Round 10 — first playable build, and Codex as an adversarial GM. 9 Aug 2026
|
||
|
||
Built: 18 talents in four categories with a Talents tab; equipment to 27 gear, 18
|
||
weapons, 10 armour; **76 custom flat SVG icons** on every item and actor, replacing
|
||
all stock Foundry art; icons wired through the skills, combat, gear and talents
|
||
lists. Verified in the running world: 27/27 items on a pregen carry custom art.
|
||
|
||
Codex reviewed it as a cooperative-but-adversarial GM and the verdict is harsh and
|
||
largely correct.
|
||
|
||
`R-103` **My "284 documents clean" claim in `R-101` was false.** The scenario's
|
||
opening was still, almost verbatim, the Ringworld case: "two falans", "four hundred
|
||
thousand kilometres to antispinward", "starboard rim wall", "spaceport ledge",
|
||
"Architect freeway". Every sweep I ran passed because every sweep searched a word
|
||
list built from the *previous* sweep — and none of those words was ever on it. The
|
||
README still read "Basic Roleplaying on the Great Arch" and so did the engine's own
|
||
first line.
|
||
|
||
This is the fourth time a rename sweep of mine has passed its own check and failed
|
||
someone else's, and the first time I published a false all-clear on the strength of
|
||
it. **A sweep that greps for the terms you already thought of cannot tell you
|
||
whether you thought of enough terms.**
|
||
|
||
Fixed: the case opening is now an actual Custodians brief — a Custodian team missing
|
||
eleven days, a beacon still transmitting, something holding a crossing open that
|
||
should not be able to. 329 documents now clean against a term list that includes
|
||
the megastructure vocabulary I had never checked for.
|
||
|
||
`R-104` **Kit and training bonuses were silently colliding.** `gearBonusFor()`
|
||
iterated *every* embedded item, so talents were swept into the gear pool and the
|
||
"best single item applies" guard then discarded one of them — a medical kit and
|
||
Triage collapsed into a single +20 rather than +40. Now two separate pools that
|
||
stack, with best-of-one *within* each. Verified: Tomas 78% → 118%.
|
||
|
||
## Codex's findings not yet fixed — all confirmed by inspection
|
||
|
||
| | |
|
||
|---|---|
|
||
| **The talents are cosmetic.** No runtime reads `grantsAction`, `usesPerCase`, `usedThisCase` or talent `coherenceCost`. There is no Use control. Only the numeric `skillBonus` does anything, and only by the accident fixed above. | Large build |
|
||
| **First contact has no UI.** `resolveContact()` is API-only; the case sheet has no READ / SIGNAL / OFFER buttons, so the scenario's forced contact stalls the session dead. | High |
|
||
| **The case cannot be completed through the UI.** No resolve-step control for research projects, no Operate/Diagnose/Repair buttons, no way to earn the Discovery except from the console. | High |
|
||
| **Group rewards are inert arrays** — impression moves, nothing it promises happens. | High |
|
||
| **The journal contradicts the engine** — it says Navigate governs phases; the engine prefers Tradecraft. | Medium |
|
||
| **Ceyla has Cover Work but no Fast Talk skill**, so her signature talent cannot fire. | Medium |
|
||
| Research as written is ~44 days plus 34 hours of repair — not one session without a montage procedure that does not exist. | Design |
|
||
|
||
**Scoreboard.** Ten rounds, 104 findings. The pattern of this round: everything
|
||
*visible* was built and works — sheets, icons, talents on the page — and almost
|
||
nothing behind the newest content is wired to a button. Codex's single most useful
|
||
sentence: *"the exact first-session stall is 'The Quiet Neighbours intercept you.
|
||
Now what button do I press?' There is none."*
|
||
|
||
# Round 10b — contact and device controls wired. 9 Aug 2026
|
||
*Answering Codex's two highest-severity findings: the first-session stall and the
|
||
uncompletable case.*
|
||
|
||
`R-105` **First contact now has controls.** The case-file contacts tab carries
|
||
READ / SIGNAL / OFFER / New encounter per group, with live impression, standing,
|
||
pidgin and offer-spent state. Choosing who talks opens a picker **ordered by who is
|
||
actually best at that stage, with the rating shown**, so the obvious choice is the
|
||
default and the interesting choice is still one click away. Verified end to end:
|
||
picker listed Tomas 58% (insight) above Ceyla 46% above Dunn 25%, and READ resolved
|
||
to chat. OFFER greys out once spent and New encounter restores it.
|
||
|
||
`R-106` **Device functions now have buttons.** Operate / Diagnose / Repair appear
|
||
under any device on the gear tab, each showing its own live blocker rather than a
|
||
generic disabled state — "Locked — the discovery that explains this has not been
|
||
made", "Too damaged. Needs Integrity 3; it has 0", "Not enough Clearance. Needs 4;
|
||
the case has 2". `useDeviceFunction()` resolves the roll, applies familiarity after
|
||
the Discovery, charges Clearance and Coherence, moves Integrity on repair, and
|
||
posts the function's own printed outcome text.
|
||
|
||
Verified as a chain: Integrity 3 alone stays locked → earning the Discovery unlocks
|
||
it → Operate succeeds, charges **4 Clearance and 1 Coherence**, and dropping
|
||
Clearance to 2 re-blocks it with the reason stated.
|
||
|
||
`R-107` **A cost of 4 was being charged as 0.** `powerCostAfterDiscovery` is absent
|
||
from the source data but the data model fills absent keys with **null**, so the
|
||
guard `f.powerCostAfterDiscovery !== undefined` passed and `Number(null)` gave 0 —
|
||
Operate ran free. Now `!= null`. Caught only because I checked the track moved
|
||
rather than that the button worked, which is the rule from `R-99` doing its job.
|
||
|
||
Still open from round 10: the talents remain mechanically inert (no Use control, no
|
||
reader for `grantsAction` / `usesPerCase` / talent `coherenceCost`), group rewards
|
||
are still inert arrays, research projects have no resolve-step control, and the
|
||
journal still says Navigate where the engine prefers Tradecraft.
|
||
|
||
# Round 10c — talents made mechanical. 9 Aug 2026
|
||
*Codex round 10 finding 4: "a cosmetic talent release, not a mechanical one."*
|
||
|
||
`R-108` **Talents now do things.** The split that makes this work is ACTIVE versus
|
||
PASSIVE, decided by the data rather than by hand: a talent is active if it costs
|
||
Coherence, is limited per case, or bends an action — those get a Use button.
|
||
Everything else is passive, has no button, and is read at the point it applies.
|
||
That prevents the failure mode where a sheet is covered in buttons that mostly say
|
||
"yes, you still have this talent".
|
||
|
||
Active, verified: **Echo Sense** charges 1 Coherence (10 → 9). **Clean Scene** is
|
||
1/case, arms, refuses a second use, and its flag clears when spent. **Requisition**
|
||
trades 1 Clearance for 2 Resources or the reverse, refusing when neither track can
|
||
pay.
|
||
|
||
Passive, verified: **Chain of Custody** absorbs a transposed cost entirely (a
|
||
1-point crossing key now costs Rakhi nothing). **Triage** adds a point to First Aid
|
||
(Ceyla 5 → 9 where a plain success gives 3). **Read the Room** learns a second fact
|
||
on a successful READ (Tomas learned 2 in one roll). **Debriefer** gives another
|
||
agent an extra point at debrief. **First Through** and **Out of Step** bend Strike
|
||
Rank to 1 and by −1 respectively.
|
||
|
||
`R-109` **`endOfCase` reset talents after its early return**, so a character who
|
||
ticked no skills kept every talent spent — permanently, for the rest of a campaign.
|
||
Moved above the return: end of case is end of case whether or not anything was
|
||
learned. Verified: clean_scene 1/1 → 0/1 with zero ticked skills.
|
||
|
||
This also answers Codex's `Chain of Custody` concern in the affirmative — it *does*
|
||
reduce a 1-cost item to free, which removes the central tension for that one agent.
|
||
Flagged for the owner rather than silently nerfed; the talent is doing exactly what
|
||
it says, and whether what it says is right is a design call.
|
||
|
||
Still open: group rewards remain inert arrays, research projects have no
|
||
resolve-step control, and the journal still says Navigate where the engine prefers
|
||
Tradecraft.
|
||
|
||
# Round 11 — generators, and Codex's verdict on them. 9 Aug 2026
|
||
*Owner: "press a button and a character is created", with full customisation
|
||
still available, plus NPCs, monsters and things in between.*
|
||
|
||
Built `generateCharacter()` and `generateNPC()`, both callable with **no arguments
|
||
at all**; a dialog that opens pre-randomised so the fastest path is open → Create;
|
||
buttons on the Actors sidebar; 5 roles, 4 experience tiers, 6 NPC threat tiers,
|
||
3 species, and a count field for 1-12 at once.
|
||
|
||
Codex reviewed as an adversarial GM. Verdict: *"Do not ship this as fully formed
|
||
character generation. It is a competent statblock seeder."* It was right.
|
||
|
||
`R-110` **Most combat NPCs could not use their own weapons.** The threat tier chose
|
||
the weapon while an unrelated random PC role chose the skills, so the two never had
|
||
to agree. Codex's measured numbers: Trained received no weapon at all, Armed 44%,
|
||
Dangerous 20%, **Apex 0%** — eight Apexes in ten had no usable attack. Replaced
|
||
`weapons: [...]` with `attacks: [{weapon, skill}]`, granted as one invariant.
|
||
Verified 10 of each tier: **60/60 weapons now have their skill**.
|
||
|
||
`R-111` **"Make four" produced four copies of one character.** The dialog resolved
|
||
"random" ONCE and passed the same role to every actor in the loop, with species and
|
||
tier hard-defaulted. Now every choice resolves per actor, "Random (rolled
|
||
separately for each)" is an explicit option, and party mode guarantees role
|
||
coverage when several PCs are made with the role left random. Verified: a party of
|
||
four came out with three distinct top skills instead of four identical sheets.
|
||
|
||
`R-112` **The generator was rolling characteristics that do not exist.** It rolled
|
||
`app` and `mas`; the data model has `str siz con int pow dex cha edu`. The Vesh
|
||
`app: -2` shift was therefore modifying a dead field — species generation was
|
||
mechanically lying. Also wrote `origin.homeWorld` where the sheet reads
|
||
`origin.world`, so every generated character had a blank home world.
|
||
|
||
`R-113` **Two shipped weapons had no skill to roll.** The Collapsible baton needs
|
||
`melee_weapon:club` and the Error cutter needs `transposed_weapon`; neither existed
|
||
in the 50-skill catalogue. This was a **content bug affecting players**, not just
|
||
the generator — anyone handed a baton could not attack with it. Catalogue now 52.
|
||
|
||
`R-114` **The Cover Work class of bug, reproduced and then closed.** A generated
|
||
talent whose `skillBonus` names a skill the character was never granted is dead on
|
||
arrival. The generator now grants that skill. Verified across all five roles: zero
|
||
dead talents, zero unusable weapons. Also added: everyone carries a **personal
|
||
anchor** (the character hook and the only Coherence recovery route) and an agency
|
||
handset, reserve ammunition is stocked at twice capacity, and the Liaison's
|
||
duplicated Trade Cant is deduplicated.
|
||
|
||
Still open from Codex round 11: Containment starts encumbered by its own issued
|
||
kit; the bonus-point spray can produce one absurd skill; blank biography, fixed age
|
||
30 and identical portraits; and the gap between Armed and Anomalous where most
|
||
Custodians cases actually live.
|
||
|
||
# Round 11b — encumbrance, biography and portraits. 9 Aug 2026
|
||
|
||
`R-115` **Everyone is issued a full kit; nobody carries all of it.** Containment
|
||
began every case at +0.78 SR for owning its own breaching charges. `stowOverload()`
|
||
now marks the heaviest items `carried: false` after kitting, so the kit is *issued*
|
||
and the player decides what comes off the vehicle. Containment is now +0.00 to
|
||
+0.34 depending on the STR roll, and every other role is exactly 0.
|
||
|
||
Two flaws in the first version of that logic, both found by reading the output
|
||
rather than trusting it:
|
||
|
||
- It stowed **everything** when it could never get under, because heavy armour is
|
||
never stowable — including shedding a 0.1 kg keepsake to fix a 4 kg overload.
|
||
It now only stows an item when that item is big enough to matter, and gives up
|
||
gracefully instead of stripping the character.
|
||
- The **personal anchor was being stowed**. It is the character hook and the only
|
||
Coherence recovery route, and it weighs 0.1 kg. It is now never stowable, and a
|
||
secondary melee weapon is, because an entry bar can be slung.
|
||
|
||
The role's key characteristic is now the best of three rolls rather than two, so a
|
||
Containment specialist is actually strong enough to carry what Containment is
|
||
issued. The residual load on a low-STR Containment agent is the 11 kg breaching
|
||
suit, which *should* slow you down and is left alone deliberately.
|
||
|
||
`R-116` **Biographies and portraits.** Every generated character gets a short
|
||
service record — role and tier, an origin drawn from their home gravity, how they
|
||
were recruited, a role-specific observation, and an **anchor**, which ties the
|
||
character hook to the Coherence rule rather than leaving it as colour. Ages are
|
||
26-54 instead of a fixed 30. 25 new portrait icons: ten baseline, five Vesh (mask),
|
||
five Cadence (three-body), five threat. Verified 4/4 distinct across a party, and
|
||
species-appropriate.
|
||
|
||
A cluster of three Physicians in one sample of four looked like a distribution bug.
|
||
It was not — over 24 characters all five roles appeared 2-7 times and all six
|
||
anchors 2-6 times. Checked rather than assumed, and left alone.
|
||
|
||
# Round 13 — Reaction Lanes replaces Strike Rank. 9 Aug 2026
|
||
*Owner rejected Strike Rank as "too complex for Foundry"; then added that the game
|
||
must also run at a physical table with paper sheets, and that the random element
|
||
must be no more than 30-35% of the outcome.*
|
||
|
||
`R-117` **Strike Rank removed entirely and replaced by Reaction Lanes.**
|
||
|
||
Reflex = ceil(DEX / 3)
|
||
Awareness = ceil(readiness / 10)
|
||
Reaction Base = 2 x max(Reflex, Awareness)
|
||
Initiative = Current Base + d4 HIGHEST acts first
|
||
|
||
Quick thought fully substitutes for quick hands — `max()` rather than a sum is what
|
||
removes the combat-training bias. Verified live: Ceyla reaches base 12 through
|
||
awareness, Dunn reaches the same 12 through reflexes, Tomas through awareness,
|
||
Rakhi 10.
|
||
|
||
**The randomness ceiling drove the die size.** Measured as
|
||
`die range / (die range + printed-base range)` over a realistic envelope
|
||
(DEX 9-18, readiness 30-60):
|
||
|
||
| | share |
|
||
|---|---:|
|
||
| `3 x Lane + d6` as first proposed | 35.7% — over |
|
||
| Dual-Axis `Reflex + Awareness + d6` | 45.5% — well over |
|
||
| **`2 x Lane + d4`** | **33.3%** |
|
||
|
||
A d6 could never have worked: its range of 5 is too close to the entire printed
|
||
spread. Probabilities that follow: trained-and-advantaged versus
|
||
untrained-and-dragged is **100%** (the gap is 6, the largest opposed d4 swing is 3),
|
||
one band apart is **87.5%**, same band is a genuine **50%**.
|
||
|
||
`R-118` **The paper sheet drove the design.** The base is calculated once at
|
||
character creation and never again. Beneath it sits a seven-box **modifier rail**
|
||
(−3 to +3) filled in once; a clip marks the box in force. Aim, Edge, Drag and Load
|
||
move the clip — they never trigger a recalculation. At the table a round is: move
|
||
the clip if anything changed, roll one d4, add it to the marked number. **One die,
|
||
one addition, no lookup.** The digital sheet renders the same rail.
|
||
|
||
`R-119` **Coherence Flux without breaching the ceiling.** An additive ±1/±2 Flux
|
||
would have pushed the random share to 45-54%, so Flux **consumes the die instead of
|
||
adding to it**: at Coherence 3-5 the outer faces misfile (1 reads as 4, 4 as 1); at
|
||
0-2 every face inverts. The distribution stays uniform 1-4, so the ceiling and the
|
||
probability tables are untouched — but the die visibly lies, and a misfiled agent
|
||
acts at the wrong moment. Verified: Rakhi at Coherence 1 rolled a 3 and it was read
|
||
as a 2.
|
||
|
||
**Four self-inflicted breakages during the excision**, all caught by reading the
|
||
console rather than assuming: removing the Strike Rank block also deleted the
|
||
module-level `ceil` helper, `readinessRating()`, and `percentileFaces()`, each of
|
||
which other subsystems depended on; and the `game.ringbrp` API object silently
|
||
failed to attach because it referenced one of them. A single undefined name in an
|
||
object literal takes the whole init hook down with no error attributable to the
|
||
system. Restored, and the API block is now verified by script for undefined
|
||
identifiers.
|
||
|
||
# Round 13b — the invisible die, and the d6. 9 Aug 2026
|
||
|
||
`R-120` **The initiative die was never shown.** `rollReaction()` evaluated a `Roll`
|
||
and read `.total`, then posted a plain `ChatMessage` with rendered HTML. Foundry
|
||
only hands dice to Dice So Nice when a message actually **carries** the Roll, so
|
||
nothing animated and the roll was invisible — the owner reasonably concluded there
|
||
was no random element at all.
|
||
|
||
Fixed by pooling every combatant's die into one `Roll` via `PoolTerm.fromRolls()`
|
||
and attaching it to the chat card with `rolls: [pool]`. Verified: the card now
|
||
reports `isRoll: true` with **4 dice objects**, and `showForRoll` fires **once** for
|
||
the whole group.
|
||
|
||
A first attempt awaited `game.dice3d.showForRoll()` per combatant inside
|
||
`rollReaction`. That worked but serialised one full animation per character and
|
||
made rolling for a party hang long enough to time out. Removed — the pooled roll on
|
||
the message animates them all together.
|
||
|
||
`R-121` **d4 → d6 at the owner's request.** Random share rises from **33.3% to
|
||
45.5%** of the printed-base spread. That is above the 30-35% ceiling he set
|
||
earlier, and he accepted the trade knowingly. Observed spread across the party went
|
||
from 12-17 to 14-17 with more movement between rounds.
|
||
|
||
`R-122` **OPEN — three of the four agents share Reaction Base 12, and it is not a
|
||
bug.** Readiness 51-60 all round to Awareness 6, and Ceyla 55, Dunn 55 and Tomas 58
|
||
are genuinely within three points of one another. The banding is faithfully
|
||
reporting that the characters are similar. Options measured:
|
||
|
||
| awareness divisor | distinct bases | spread | cost |
|
||
|---|---:|---:|---|
|
||
| /10 (current) | 2/4 | 2 | — |
|
||
| /8 | 3/4 | 4 | awareness begins to dominate DEX |
|
||
| /7 | 3/4 | 6 | Dunn's DEX 18 stops mattering |
|
||
|
||
No change made: separating them means making awareness beat reflexes more often,
|
||
which is a design decision for the owner rather than a defect to fix.
|
||
|
||
# Round 14 — pregens differentiated, all content as Items, combat playtested. 9 Aug 2026
|
||
|
||
`R-123` **The pregens now pull the two initiative axes apart.** Three of four shared
|
||
Reaction Base 12 because their readiness was 55/55/58 — the mechanic was faithfully
|
||
reporting that the characters were nearly identical. Rebuilt so each sits clearly
|
||
on one axis:
|
||
|
||
| | DEX | reflex | readiness | aware | base | led by |
|
||
|---|---:|---:|---:|---:|---:|---|
|
||
| Ceyla | 10 | 4 | 71 | 8 | **16** | awareness |
|
||
| Dunn | 18 | 6 | 34 | 4 | **12** | reflex |
|
||
| Tomas | 14 | 5 | 58 | 6 | **12** | awareness |
|
||
| Rakhi | 11 | 4 | 47 | 5 | **10** | awareness |
|
||
|
||
Three distinct bases, spread 6. Ceyla is now the slowest on her feet and reliably
|
||
the first to act, which is the design working: she has usually worked out what is
|
||
happening a beat before it happens. Dunn and Tomas tie at 12 by *different routes* —
|
||
reflexes and awareness — which is the point of `max()` rather than a sum. Bios were
|
||
rewritten to match the numbers.
|
||
|
||
`R-124` **All content installed as Items.** `installContent()` now covers every pack
|
||
— skills, talents, weapons, armour, gear and devices — **125 items** browsable and
|
||
draggable in the Items directory, not just the 70 skills and talents.
|
||
|
||
## Combat playtest — three defects found and fixed
|
||
|
||
`R-125` **HP was calibrated for weapons that no longer exist.** `hitPointsFor` was
|
||
`CON + SIZ`, double standard BRP, set deliberately when the weapon ladder was the
|
||
1984 one (laser rifle 1d10+30). The setting rewrite replaced those with modern
|
||
agency issue averaging 3.5-9 damage, and HP was never brought down. Measured
|
||
result: **ten rounds of melee with nobody dropped and armour absorbing 51% of all
|
||
damage**. Now `(CON + SIZ) / 2` — Dunn 23 → 12, Rakhi 29 → 15.
|
||
|
||
Note for the owner: the Major Wound threshold is unchanged at 9, because the floor
|
||
was already doing all the work at both scales. But 9 is now 60-75% of a character's
|
||
pool rather than 35%, so a Major Wound is a much bigger event than it was. That
|
||
follows from your "floors at 9" ruling meeting the new HP scale — worth a look.
|
||
|
||
`R-126` **Human NPCs had natural armour.** Cleaners carried `naturalArmour: 2` on
|
||
top of a stab vest, an alien-hide leftover from the old setting, making ordinary
|
||
contractors better protected (4 AP) than the agents shooting at them (2-3 AP). Set
|
||
to 0 for all human NPCs; the Revenant keeps its 4 because it is not a person.
|
||
|
||
`R-127` **The compact pistol could not hurt anything.** 1d6 against 4 AP did
|
||
**nothing 66% of the time**, average 0.51 through — and it is the sidearm issued to
|
||
the Physician and Liaison roles, so those characters simply could not take part in
|
||
a firefight. Raised to 1d6+2, still the weakest firearm but functional.
|
||
|
||
Also: **Dunn was issued a baton but trained in knife**, so he was swinging at his
|
||
untrained 5% club skill. Reissued a utility knife.
|
||
|
||
## Result, 10 runs of each
|
||
|
||
| | before | after |
|
||
|---|---|---|
|
||
| Ranged, 2 PCs vs 2 Cleaners | **PC 0/10**, avg 8.4 rounds | **PC 7/10**, avg 8 rounds |
|
||
| Melee, 2 PCs vs 2 Keepers | PC 10/10, avg 6.5 | PC 10/10, avg 6 |
|
||
| Ranged, full team vs 3 Cleaners | not run | **PC 10/10**, avg 4.4 |
|
||
|
||
Armour now absorbs 28-39% rather than 40-51%. A fair fight is genuinely in doubt;
|
||
a full team against a smaller force wins in about four rounds. Melee remains
|
||
strongly PC-favoured, which is Rakhi: an entry bar with a +1d6 damage modifier
|
||
against unarmoured scavengers is meant to be decisive.
|
||
|
||
# Round 14b — Major Wound at half hit points. 9 Aug 2026
|
||
|
||
`R-128` **Major Wound is now `ceil(HP / 2)`**, replacing `max(9, HP/3)`.
|
||
|
||
The floor of 9 had been doing all the work at both the old and new HP scales, so
|
||
once HP halved it became 60-75% of a character's whole pool and a Major Wound
|
||
almost never fired. Half HP restores it to what it is meant to be: a blow that
|
||
takes you out of the fight, not one that nearly kills you outright.
|
||
|
||
Dunn HP 12 → MW 6 Rakhi HP 15 → MW 8
|
||
Tomas HP 13 → MW 7 Ceyla HP 13 → MW 7
|
||
|
||
A duplicate copy of the formula in `tools/content.mjs` still carried the old
|
||
`max(9, hp/3)` and was silently disagreeing with the engine — the packs were built
|
||
with one rule and played with another. Aligned.
|
||
|
||
Dunn's biography also still explained the retired floor ("his threshold would be 8
|
||
by the arithmetic; the floor raises it to 9") and carried a mangled phrase left by
|
||
an earlier blanket rename. Rewritten.
|
||
|
||
## Measured effect, 10 runs each
|
||
|
||
| | result | rounds | majors |
|
||
|---|---|---:|---|
|
||
| Ranged 2 v 2 | PC 8/10 | 6.9 | 23% of hits |
|
||
| Melee 2 v 2 | PC 10/10 | 4.9 | 19% of hits |
|
||
| Ranged 4 v 3 | PC 10/10 | 4.2 | 50% of hits |
|
||
|
||
Roughly one hit in five now causes a Major Wound in an even fight, and the ranged
|
||
2 v 2 improved from 7/10 to 8/10 with a round shaved off. The 50% figure in the
|
||
4 v 3 is the team's heavy weapons — Rakhi's marksman rifle and Ceyla's Error lance
|
||
— against 2 AP contractors, which is what those weapons are for.
|
||
|
||
# Round 14c — wound effects exercised and made real. 9 Aug 2026
|
||
*The hit-location path had never been run inside a fight; every previous playtest
|
||
used `resolveHit`, which skips locations entirely.*
|
||
|
||
`R-129` **Location sizes had not been rescaled with HP.** Fractions were set
|
||
against the old 26-point pool, so halving HP left a leg at 4 hit points against a
|
||
sidearm doing 4-6 through armour. Raised and, more importantly, set **explicitly by
|
||
location kind** rather than by chained string replacement — an earlier attempt at
|
||
this corrupted the values into `0.280` and `0.425` because each replacement ran over
|
||
the output of the last.
|
||
|
||
leg .42 · body .42 · vital .50 · arm .35 · head .40 · unit .28
|
||
|
||
Measured over 150 clean hits each, against armour 3:
|
||
|
||
| weapon | disables the limb | major wound |
|
||
|---|---:|---:|
|
||
| Compact pistol | 5% | 0% |
|
||
| Service pistol | 29% | 23% |
|
||
| Marksman rifle | 46% | 43% |
|
||
| Entry bar +1d6 | 56% | 53% |
|
||
|
||
A sidearm rarely cripples, a rifle often does, a heavy melee weapon usually does.
|
||
|
||
`R-130` **My own measurement was wrong first, and it was the R-93 trap again.**
|
||
The initial reading was "every hit disables a location, 6 of 6" and then "97-100%".
|
||
Both were artefacts: my harness reset with `{"system.locationDamage": {}}`, and
|
||
**Foundry merges object updates**, so nothing was ever cleared. Accumulated `taken`
|
||
values reached 382 and 434, which of course disabled everything. The fix is the
|
||
`-=` deletion form, exactly as recorded in `R-93` — and I did not apply my own
|
||
earlier lesson. The system was closer to correct than my test said.
|
||
|
||
`R-131` **Wound effects were printed and never applied.** The chat cards were
|
||
right — "The arm is useless. Two-handed actions are impossible; −30% Manipulation",
|
||
"A vital hit. Knocked down, and a Stamina roll or unconscious" — but nothing read
|
||
`disabledLocations`, and a *destroyed* leg left MOV at 10. Now derived in
|
||
`prepareDerivedData` and applied at the `skillRoll` choke point alongside
|
||
encumbrance and Coherence:
|
||
|
||
| state | MOV | effect |
|
||
|---|---:|---|
|
||
| healthy | 10 | shot at 59% |
|
||
| arm destroyed | 10 | shot at **29%** (−30 Manipulation) |
|
||
| leg disabled | **5** | −30 Physical |
|
||
| leg destroyed | **1** | prone |
|
||
|
||
Clean six-round firefight afterwards: 9 hits, 4 crippled limbs, 3 major wounds,
|
||
two shots stopped dead by armour, PCs won with Dunn at 7/12 and a battered leg.
|
||
|
||
# Round 14d — combat with wound effects live. 9 Aug 2026
|
||
|
||
The penalty is visibly load-bearing. From a narrated firefight, round 5:
|
||
|
||
FOE 45%-30= 15% roll 6 success
|
||
|
||
— a Cleaner whose arm had been crippled two rounds earlier, now shooting at 15%
|
||
instead of 45%. Dunn finished the fight on 1 hit point with MOV 5 and a ruined leg.
|
||
|
||
Across 12 runs of each engagement:
|
||
|
||
| | result | rounds | majors |
|
||
|---|---|---:|---:|
|
||
| Ranged 2 v 2 | PC 8/12 | 6.4 | 36 |
|
||
| Melee 2 v 2 | PC 12/12 | 5.5 | 22 |
|
||
| Ranged 4 v 3 | PC 12/12 | 4.4 | 57 |
|
||
|
||
**No death spiral.** Only 8-14 shots per engagement were fired by an already-wounded
|
||
combatant, because fights resolve before the penalty compounds. The wound matters
|
||
in the moment it lands without dragging the loser through a hopeless tail.
|
||
|
||
`R-132` **`destroyed` was tested against a single blow while `disabled` was
|
||
cumulative.** `resolveLocationHit` used `d >= max * 2` — this hit's damage — so a
|
||
limb could never be destroyed by repeated wounds, only by one enormous strike. The
|
||
actor's own derived data meanwhile computed `destroyed` from *cumulative* damage, so
|
||
the resolver and the sheet disagreed about the same limb: a location carrying 100
|
||
points of damage against a maximum of 6 reported `destroyed: false` from the
|
||
resolver and `destroyed: true` on the sheet. Now cumulative in both places.
|
||
|
||
That is the fifth duplicated-rule defect in this project (`R-97`, `R-100`, `R-104`,
|
||
`R-128`, and now this) — a rule implemented twice and changed in one place.
|
||
|
||
**Caveat on an earlier figure.** The "47% of hits crippled" in the trials counts
|
||
repeat hits on a limb that was *already* crippled, because `disabled` correctly
|
||
stays true once crossed. The honest per-hit rate is the controlled measurement:
|
||
5% for a sidearm, 29% service pistol, 46% marksman rifle, 56% entry bar.
|
||
|
||
# Round 15 — the duplicated-rule class, fixed at the root. 9 Aug 2026
|
||
|
||
Five separate defects in this project came from one shape: a rule implemented in
|
||
two places and changed in one — `R-97` the medical kit's bonus, `R-100` the phase
|
||
lead, `R-104` kit versus training bonuses, `R-128` Major Wound, `R-132` limb
|
||
destruction. Fixing them individually was not working, so this round fixes the
|
||
cause.
|
||
|
||
`R-133` **`tools/content.mjs` carried its own copies of the engine's rules**, under
|
||
a comment that said so out loud: *"rule functions mirrored from ringbrp.mjs"*. They
|
||
had drifted twice. Major Wound was `max(9, hp/3)` in the builder while the engine
|
||
used `hp/2`; and — found live during this audit — **hit points were still
|
||
`CON + SIZ` in the builder after the engine had halved them**, so the shipped packs
|
||
carried Ceyla at 26 HP and Rakhi at 29 while the game computed 13 and 15. Derived
|
||
data masked it on load, which is why playtesting never caught it.
|
||
|
||
**Fixed structurally.** New `rules.mjs` is the single authority for every pure rule
|
||
formula — hit points, Major Wound, damage modifier, marksmanship, location size,
|
||
the two Reaction axes and the base, encumbrance. It imports nothing from the
|
||
runtime and touches no Foundry globals, so both sides can use it: `ringbrp.mjs`
|
||
imports it in the browser, `tools/content.mjs` imports it in Node. Every local copy
|
||
deleted; verified zero re-definitions remain in either file.
|
||
|
||
`R-134` **A guard, so the class cannot come back.** `tools/check-rules.mjs`:
|
||
|
||
1. fails if anything outside `rules.mjs` re-defines a canonical rule,
|
||
2. asserts the builder's aliases *are* the canonical functions rather than copies
|
||
with the same name,
|
||
3. spot-checks eight formulas against known values.
|
||
|
||
`build-packs.mjs` now runs it first and **refuses to build** on failure. Proven by
|
||
re-introducing the exact `hitPoints = con + siz` regression: the check failed with
|
||
`content.hitPoints is not the same function as rules.hitPointsFor` and exit code 1.
|
||
|
||
Verified after rebuild: packs store 13/15/12/13 HP with Major Wounds 7/8/6/7, and
|
||
sheet and engine agree on every pregen for the first time.
|
||
|
||
`R-135` **Codex found the one the guard missed: constants are rules too.** The first
|
||
version of `check-rules.mjs` policed rule *functions* only. `ringbrp.mjs` still held
|
||
`RINGBRP.reaction = { dexDivisor: 3, readinessDivisor: 10, laneMultiplier: 2, die: 6,
|
||
modCap: 3 }` — a second literal copy of `rules.REACTION`, with its own copy of the
|
||
divisors — and two places recomputed `laneMultiplier * max(reflex, aware)` inline
|
||
rather than calling `reactionBaseFrom()`. Exactly the defect being fixed, surviving
|
||
inside the fix. Now `RINGBRP.reaction = REACTION` by assignment, both call sites go
|
||
through the canonical function, and the guard fails on any object literal carrying
|
||
two or more keys of a canonical constant set.
|
||
|
||
`R-136` **Discovery made the guard weaker, and the regression test caught it.**
|
||
Deriving the alias list by scanning for `export const X = Y;` meant that replacing an
|
||
alias with the drifted arrow function `(con, siz) => con + siz` stopped matching the
|
||
discovery regex — so it was never checked and the guard reported OK. The original bug
|
||
passed. Fixed by making a hand-held `REQUIRED` map authoritative, with discovery only
|
||
able to ADD: a missing alias now fails as loudly as a wrong one.
|
||
|
||
`R-137` **The same drift in prose.** The Reaction comment still described a d4 and
|
||
quoted d4 probabilities after the die changed to d6; `README.md` still advertised
|
||
`CON + MAS`, `max(9, ceil(HP/3))` and gravity-based marksmanship. Both corrected, and
|
||
the code comment no longer restates any formula — it points at `rules.mjs` and carries
|
||
only measured behaviour.
|
||
|
||
**Guard now fails on four escape routes, each verified by deliberate regression:**
|
||
an alias swapped for a drifted copy; an alias deleted; a rule defined in a brand-new
|
||
file; a rule added to `rules.mjs` with no spot-check. Plus the constant-literal case.
|
||
|
||
**Measured, and worth the owner knowing:** on the shipped pregens the d6 puts the
|
||
random share at **45.5%**, above the 30–35% ceiling that was set. One band apart wins
|
||
72.2% (81.3% excluding ties). The d6 was chosen deliberately for table feel over the
|
||
d4 that hit 33.3%. `REACTION.laneMultiplier: 2 -> 3` returns the share to 35.7%
|
||
without touching the die — a one-line change, left undone because it was not asked for.
|
||
|
||
`R-138` **laneMultiplier 2 -> 3, at the owner's instruction.** The random share is
|
||
now **35.7%**, inside the 30–35% ceiling. Verified live: bases 24 / 18 / 18 / 15.
|
||
|
||
Measured on the shipped party over 200,000 rounds, both settings, load applied:
|
||
|
||
| | share | Ceyla | Dunn | Tomas | Rakhi |
|
||
|---|---:|---:|---:|---:|---:|
|
||
| laneMultiplier 2 | 45.5% | 73.1% | 14.8% | 12.1% | 0.0% |
|
||
| **laneMultiplier 3** | **35.7%** | **94.8%** | **2.8%** | **2.4%** | **0.0%** |
|
||
|
||
The ceiling was bought with order. Cutting the die's share does not spread the lead
|
||
around — it concentrates it. Ceyla goes from a strong favourite to a near-certainty,
|
||
and the two mid agents lose most of their share of the lead. Same band is still a
|
||
50/50 (16.7% ties, broken by DEX) and one band apart is 83.3%, but **two bands apart
|
||
is now 100%**: a six-point gap cannot be closed by a d6 at all.
|
||
|
||
Rakhi never leads at either setting, so that is not caused by this change. It is her
|
||
−2 Load from carrying 38 kg against a free carry of 18. Dropping roughly 20 kg returns
|
||
her to 15 and puts her level with Dunn and Tomas — a real decision on the sheet rather
|
||
than a fixed disadvantage, which is the intent.
|
||
|
||
`R-139` **A third duplicate, found while measuring.** `prepareDerivedData` recomputed
|
||
encumbrance inline — its own `Math.floor(over / 10)`, `steps * 5` and `over / 10` —
|
||
instead of calling `encumbranceFrom()`. It also wrote the over-weight under the key
|
||
`overKg` while `rules.mjs` returned `over`, so simply routing the call through the
|
||
canonical function would have silently zeroed every encumbrance penalty, because
|
||
`loadBand()` reads `overKg`. Fixed by returning both names from one computation and
|
||
saying in the source why both exist. The inline block is gone.
|
||
|
||
**Still open.** `srLoad` — the exact fractional cost per kilogramme, written for the
|
||
old Strike Rank so no weight was ever free — is computed and displayed but no longer
|
||
used: Reaction Lanes reads `loadBand()`, a three-value integer band (0 / −1 / −2).
|
||
The granular rule the owner asked for survives in the data and not in play.
|
||
|
||
`R-140` **srLoad wired into the rail — the granular rule is in play at last.** The
|
||
exact per-kilogramme cost, computed since R-23 and consumed by nothing, now reaches
|
||
initiative. The three-value band (`0 / −1 / −2`) is gone.
|
||
|
||
**Load is not a rail position.** It is a property of what the agent is carrying,
|
||
known before the fight starts, so it is subtracted into the number printed on the
|
||
sheet rather than occupying a marker slot. The rail is now situation only — Aim,
|
||
Edge, Drag — which is what it should always have meant, and load can no longer be
|
||
double-counted by moving the clip.
|
||
|
||
| | old band | now |
|
||
|---|---:|---:|
|
||
| 0.1 kg over | −1 | −0.1 |
|
||
| 1 kg | −1 | −0.1 |
|
||
| 5 kg | −1 | −0.5 |
|
||
| 10 kg | −1 | −1 |
|
||
| 20 kg | −2 | −2 |
|
||
| 30 kg | −2 | −3 |
|
||
|
||
Gentler where it was absurd — the old band charged a full point for the first gram
|
||
over and then nothing more for the next 10 kg — and heavier where it was too kind,
|
||
since the cap at −2 meant nothing above 20 kg cost anything at all.
|
||
|
||
**Rounded up, not to nearest.** To nearest made the first half-kilogramme free, which
|
||
verified live: a 1 kg weight added to Dunn moved his printed value not at all. `ceil`
|
||
to one decimal makes 100 g over cost 0.1 and nothing cost nothing, which is the rule
|
||
as stated. One decimal is the smallest unit worth writing on a paper sheet.
|
||
|
||
**Floored at 1** — R-56 applied to encumbrance. No amount of kit reduces an agent to
|
||
acting never; it reduces them to acting last. A 300 kg overload prints 1, not −15.
|
||
|
||
`R-141` **Ordering bug caught before it shipped.** `prepareDerivedData` derived the
|
||
reaction block *before* encumbrance, so the printed value read an unset
|
||
`sys.encumbrance` and would have silently printed a load of zero for every actor —
|
||
the same silent-wrong class as the double hit points. The block is moved below
|
||
encumbrance with the dependency stated in the source.
|
||
|
||
Verified live: Ceyla 15.4 kg against a free carry of 12 prints **23.6** (base 24 less
|
||
0.4); Rakhi at 38 against 18 prints **13**. Adding 1 kg to Dunn moves him 18 → 17.9
|
||
and removing it restores 18. Turn order sorted correctly on decimals, and
|
||
`CONFIG.Combat.initiative.decimals` was already 1.
|
||
|
||
# Round 16 — combat played under the new load rule. 9 Aug 2026
|
||
|
||
Two fights, four agents against four Cleaners, opening at 25 m and closing to
|
||
contact. Driven through the real engine — `resolveBands`, `gradeRoll`,
|
||
`rangeBandFor`, `armourAgainst`, `damageAfterArmour`, `hitLocationRoll`,
|
||
`majorWoundRoll`, `woundPenaltyFor` — not a hand simulation.
|
||
|
||
**Fight one: total party kill, three Cleaners still standing.** Tomas and Ceyla
|
||
both dropped to single critical hits, which ignore armour entirely. Rakhi survived
|
||
six rounds of concentrated pistol fire behind a breaching suit and then lost to a
|
||
critical. Losing two agents in the first two rounds against equal numbers is not
|
||
recoverable.
|
||
|
||
**Fight two: agents win in five rounds, three standing, all four Cleaners down.**
|
||
The difference was Rakhi dropping her pack.
|
||
|
||
`R-142` **The load rule works, and it produced a real decision.** Rakhi begins at
|
||
38 kg against a free carry of 18 — Load −2, printing 13, the slowest lane in the
|
||
team. In round one she acted **last of eight**, behind all four Cleaners. She spent
|
||
round two dropping her pack: 17.4 kg, Load 0, printing 15. From round three she
|
||
acted third or better every round and killed a Cleaner with the prybar.
|
||
|
||
Measured over 100,000 rounds against a Cleaner printing 12:
|
||
|
||
| Rakhi | acts first | tie |
|
||
|---|---:|---:|
|
||
| carrying 38 kg (prints 13) | 58.6% | 13.9% |
|
||
| pack dropped (prints 15) | 83.2% | 8.3% |
|
||
|
||
A 24.6 point swing for one action. That is the trade the rule exists to create, and
|
||
it is legible at the table: the number on the sheet changes when the pack comes off.
|
||
|
||
`R-143` **Armour is the dominant variable in Major Wounds, far more than hit points.**
|
||
Frequency of Major Wounds among service-pistol hits that beat armour, 200,000 trials:
|
||
|
||
| | armour | MW threshold | hits through | of those, Major Wounds |
|
||
|---|---:|---:|---:|---:|
|
||
| Ceyla | 6 | 7 | 50% | **0.0%** |
|
||
| Rakhi | 7 | 8 | 38% | **0.0%** |
|
||
| Dunn | 3 | 6 | 88% | **28.6%** |
|
||
| Tomas | 2 | 7 | 100% | **24.9%** |
|
||
|
||
A pistol maxes at 10, so 6+ armour makes a Major Wound arithmetically impossible on
|
||
a normal success, while 2–3 armour takes one every fourth hit. The pressure valve is
|
||
the critical, which ignores armour — and in both fights every agent who went down
|
||
went down to a critical. That is a defensible design, but the cliff between armour 3
|
||
and armour 6 is much sharper than the two-point difference on the sheet suggests.
|
||
|
||
`R-144` **Major Wounds change no roll.** The most serious finding. `majorWoundRoll`
|
||
rolls 1d10, resolves the table, posts a chat card with the full effect text, records
|
||
the entry in `system.wounds`, and shows it on the sheet — and stops there.
|
||
`woundPenaltyFor` reads `system.wounded`, which is derived **only** from
|
||
`locationDamage`, so:
|
||
|
||
- a `manipulation` Major Wound, whose own text reads "−30% Manipulation", applies 0
|
||
- a `mobility` Major Wound, "MOV halved", leaves MOV at 10
|
||
- `concussed`, "−20% to all rolls", applies 0
|
||
|
||
Verified directly: with `manipulation` and `mobility` both recorded in
|
||
`system.wounds`, every `woundPenaltyFor` category returns 0 and MOV is unchanged.
|
||
Only `crippling` and `catastrophic` do anything at all, via the `isDown` check.
|
||
|
||
Disabled *locations* do work correctly — a destroyed leg gives `physical −30`,
|
||
`movFactor 0.1`, `prone true`, and a destroyed arm gives `manipulation −30`. It is
|
||
the Major Wound table specifically that is prose. Same class as `R-102`.
|
||
|
||
Observed live in fight two: Ceyla took a manipulation Major Wound in round two and
|
||
shot at exactly the same 53% in round three.
|
||
|
||
**Not a bug found this round:** I first reported `woundPenaltyFor` as broken because
|
||
it returned 0 for `"physical"` and `"manipulation"`. Those are `system.wounded`
|
||
field names, not category ids. The ids are `phys`, `manip`, `melee`, `ranged`,
|
||
`comm`, `mental`, `percep`, and the function handles all seven correctly.
|
||
|
||
`R-145` **Major Wound effects wired into the penalty pipeline — R-144 closed.** The
|
||
table is no longer prose. `woundEffectsFor(wounds)` in `rules.mjs` turns each row
|
||
into numbers, `prepareDerivedData` folds them into `system.wounded` on top of the
|
||
location-derived state, and `woundPenaltyFor` reads the result at the existing
|
||
`skillRoll` choke point. No new choke point was added: everything that already
|
||
respected a shattered arm now respects a concussion.
|
||
|
||
| Wound | Applied |
|
||
|---|---|
|
||
| concussed | −20% to **every** category, via a new `all` term |
|
||
| winded | −20% Physical, `lostAction` flag |
|
||
| bleeding / deepBleeding | 1 and 2 hp per round, exposed as `wounded.bleed` |
|
||
| manipulation | −30% manipulation, melee and ranged |
|
||
| mobility | −30% Physical, MOV halved |
|
||
| sensory (sight) | −40% Perception, −40% manipulation/melee/ranged |
|
||
| sensory (hearing) | −40% Perception, −40% Communication |
|
||
| broken | as manipulation or mobility, by variant |
|
||
| crippling | defence only, MOV 1, counts as down |
|
||
| catastrophic | unconscious, removed from the fight |
|
||
|
||
Three decisions worth recording:
|
||
|
||
**They stack, and they stack with locations.** Two concussions are −40%; a
|
||
concussion plus a destroyed leg is −50% Physical with MOV 1 and prone. Both facts
|
||
are true about the same agent, so the numbers add rather than one overwriting the
|
||
other. Verified live.
|
||
|
||
**The 1D2 variants are rolled and stored.** `sensory` and `broken` each split two
|
||
ways. That roll used to be left to the GM because nothing consumed it; now the
|
||
penalty depends on it, so `majorWoundRoll` rolls it, stores it on the wound entry
|
||
and prints it on the chat card. **A missing variant falls back to the harsher
|
||
reading**, so an old wound saved before this change can never quietly cost nothing.
|
||
|
||
**`isDown` now reads the derived flag** rather than re-testing the wound keys, which
|
||
was a second place that knew which wounds end a fight.
|
||
|
||
Verified through the real `skillRoll`, not a reimplementation:
|
||
|
||
```
|
||
healthy Firearm (Pistol) 59% → 59% ≤3 ≤12 ≤59 ≥98
|
||
concussed (−20 all) 59% -20% → 39% Wounded: -20% ≤2 ≤8 ≤39 ≥97
|
||
concussed + manipulation 59% -50% → 9% Wounded: -50% ≤1 ≤2 ≤9 ≥96
|
||
cleared 59% → 59% ≤3 ≤12 ≤59 ≥98
|
||
```
|
||
|
||
Critical, special and fumble bands all move with the penalty, and the chat card
|
||
names the cause. In a live fight Tomas took a catastrophic wound at 1 hp and was
|
||
correctly removed from the turn order for the rest of the encounter.
|
||
|
||
Guard extended to 13 rules and 35 formulas, including the missing-variant fallback.
|
||
|
||
**Two harness errors of mine, recorded so they are not re-found as engine bugs:**
|
||
`woundPenaltyFor` takes category ids (`phys`, `manip`), not `system.wounded` field
|
||
names; and `skillRoll` takes `rating` and `categoryId`, not a `skill` object —
|
||
passing one silently rolls against 0%.
|
||
|
||
# Round 17 — combat with wounds live, and a correction. 9 Aug 2026
|
||
|
||
`R-146` **Correction: every fight in rounds 15 and 16 ran at double damage.**
|
||
`hitLocationRoll` applies the hit points itself (`ringbrp.mjs:929`) as well as
|
||
recording the location. My harness subtracted the same damage a second time. The
|
||
engine was never wrong; the playtests were.
|
||
|
||
What this invalidates: the fight narratives in R-142 and R-144 — the total party
|
||
kill, "every agent who went down went down to a critical", and the impression that
|
||
Major Wounds rarely fire. What it does **not** touch: the load measurements in R-142
|
||
(initiative does not depend on damage) or the armour arithmetic in R-143 (computed
|
||
directly, not from a fight).
|
||
|
||
`R-147` **Wounds re-tested at correct damage, and they carry the fight.** Four agents
|
||
against two marksmen and two contractors at 30 m, twelve rounds. Eight Major Wounds
|
||
fired and every one of them changed what happened next:
|
||
|
||
| | |
|
||
|---|---|
|
||
| Dunn | **catastrophic**, round 1 — unconscious, removed from the order, never acted |
|
||
| Tomas | **manipulation** — pistol 43% → 13%, missed everything after and died at 3 hp |
|
||
| Marksman | **manipulation** — rifle 55% → 25% |
|
||
| Marksman | **broken (manipulation)** — 55% → 25%, MOV 5 |
|
||
| Rakhi | **deepBleeding** — 2/round from 10 hp, dead in round 6 |
|
||
| Marksman, Cleaner, Cleaner | **bleeding / deepBleeding** — all three bled out |
|
||
|
||
**Bleeding decides fights.** Four of the six casualties died of blood loss rather
|
||
than the blow that caused it, including Rakhi, who was on 10 hit points when she took
|
||
it. Two per round against 12–15 hit points is roughly a five-round death sentence and
|
||
there is currently no First Aid step in the loop to stop it — the rule assumes
|
||
someone spends an action, and nothing in play forces that choice to be visible. This
|
||
is the wound effect with the largest influence on outcomes and it deserves a check.
|
||
|
||
**The -30% wounds are decisive rather than flavour.** Tomas at 13% and the marksmen
|
||
at 25% were all still standing and effectively disarmed. That is the intent of the
|
||
table text, now actually true.
|
||
|
||
`R-148` **Stalemate at the tail.** Rounds 9 to 11 produced nothing: Ceyla at 53%
|
||
against a marksman at 55%, armour 6 against 1d10+2, both missing or being stopped.
|
||
Two roughly equal combatants with good armour and mid skills can circle indefinitely.
|
||
Worth a pressure mechanic — a closing clock, or escalating penalties for a stalled
|
||
exchange — rather than leaving it to the dice.
|
||
|
||
Wound keys not seen this fight — `concussed`, `winded`, `mobility`, `sensory`,
|
||
`crippling` — were verified separately against every category and MOV in round 16.
|
||
|
||
`R-149` **Bleeding fixed — the rule was missing both halves, failing in opposite
|
||
directions.** The table printed a rate and a cure and the game implemented neither:
|
||
|
||
- **Nothing ticked the loss.** `wounded.bleed` was derived and displayed, and no
|
||
code ever subtracted it. At an actual Foundry table bleeding did *nothing*.
|
||
- **Nothing stopped it.** `firstAid` healed hit points and never cleared the wound,
|
||
so once my harness applied the loss it ran until the target was dead.
|
||
|
||
That is why R-147 read as a death sentence: my simulation supplied the half the
|
||
engine was missing and none of the half that ends it.
|
||
|
||
**Both halves now implemented.** `BLEED` in `rules.mjs` is the single authority for
|
||
the rate and for what stops it; `woundEffectsFor` reads it rather than carrying its
|
||
own copies of 1 and 2.
|
||
|
||
| | per round | stopped by |
|
||
|---|---:|---|
|
||
| bleeding | 1 | First Aid, or Medicine |
|
||
| deepBleeding | 2 | **Difficult** First Aid, or any successful Medicine |
|
||
|
||
`applyBleeding()` runs from the `updateCombat` hook; `stopBleeding()` is called by
|
||
`firstAid` on success and by the new `medicalCare()`, both routed through
|
||
`bleedStoppedBy` so there is one authority on what qualifies.
|
||
|
||
**One deviation from the printed text, deliberately.** The table stops plain
|
||
bleeding with First Aid only. Medicine now also stops it, because the rule as
|
||
written left a surgeon unable to close the lesser wound they could treat.
|
||
|
||
**A free tick, caught in testing.** The hook fired on entering round 1, charging a
|
||
bleeding agent before any round had elapsed — 15 hp became 11 by round 2 instead of
|
||
13. Now only round 2 and later charge.
|
||
|
||
**No rebalancing was needed, and I checked before assuming.** Survival with a medic
|
||
retrying each round, 50,000 trials:
|
||
|
||
| from | Difficult First Aid (23%) | Medicine-grade (46%) |
|
||
|---:|---:|---:|
|
||
| 15 hp | 87.6% | 99.3% |
|
||
| 12 hp | 78.8% | 97.5% |
|
||
| 10 hp | 72.9% | 95.5% |
|
||
| 5 hp | 54.5% | 84.3% |
|
||
|
||
Bleeding was never too harsh — it was unstoppable. Treatable, it costs the medic an
|
||
action every round and still usually works, which is the decision the rule wants.
|
||
A single unlucky live run took seven attempts; the distribution says four.
|
||
|
||
Verified live: the clock runs on round change, an Average First Aid does **not**
|
||
close a deep bleed, a Difficult one does, Athletics does not, plain bleeding closes
|
||
on an Average First Aid, and the loss stops at 0 rather than grinding negative.
|
||
|
||
Guard now covers 15 rules, 2 constant sets and 44 formulas.
|
||
|
||
# Round 18 — bleeding in play, and a defect that outranks it. 9 Aug 2026
|
||
|
||
Fights run on a real `Combat` document this time, so initiative came from
|
||
`rollReaction` and blood loss from the engine's own `updateCombat` hook rather than
|
||
anything in the harness.
|
||
|
||
`R-150` **Bleeding works in play.** Two bleeds fired in the reported fight and both
|
||
became attrition clocks rather than announcements:
|
||
|
||
```
|
||
R3 Tomas pistol 43% → Marksman 2 critical — 8 dmg (hp 6)
|
||
** BLEEDING ** bleeds 1/round
|
||
[engine bleed] Marksman -1 → 5
|
||
R4 [engine bleed] Marksman -1 → 3 ← dead to a critical the next round
|
||
R5 Dunn knife 34% → Cleaner 2 critical — 6 dmg (hp 8)
|
||
** DEEPBLEEDING ** bleeds 2/round
|
||
[engine bleed] Cleaner -2 → 6
|
||
R6 [engine bleed] Cleaner -2 → 4
|
||
```
|
||
|
||
Both wounded hostiles died at least partly to the bleed. The tick fires once per
|
||
round, from the engine, with no harness involvement. The First Aid branch did not
|
||
trigger only because both bleeds landed on hostiles; staunching was verified
|
||
directly in R-149.
|
||
|
||
Wound penalties were visible throughout: Tomas's pistol fell 43% → 23% and Rakhi's
|
||
rifle 47% → 27% from concussions, for the whole rest of the fight.
|
||
|
||
`R-151` **Initiative is rolled once and frozen for the entire combat.** The larger
|
||
find. Foundry keeps `combatant.initiative` across rounds and nothing re-rolls it, so
|
||
the d6 is drawn once per fight, not once per round. Verified over four rounds — the
|
||
same four numbers, unchanged:
|
||
|
||
```
|
||
round 1: Ceyla 25.6 · Dunn 22 · Tomas 19 · Rakhi 15
|
||
round 2: Ceyla 25.6 · Dunn 22 · Tomas 19 · Rakhi 15
|
||
round 3: Ceyla 25.6 · Dunn 22 · Tomas 19 · Rakhi 15
|
||
round 4: Ceyla 25.6 · Dunn 22 · Tomas 19 · Rakhi 15
|
||
```
|
||
|
||
Three reasons this is wrong rather than merely a choice:
|
||
|
||
1. **The Aim rail is inert.** "Aim one round, fire the next" for +1/+2 cannot do
|
||
anything if the order never recomputes. The entire seven-box rail — Aim, Edge,
|
||
Drag — only means something between rounds. It currently means nothing after
|
||
round 1.
|
||
2. **The measured random share is per round.** 35.7% was tuned as round-to-round
|
||
variance. Drawn once, the variance is 0% for the rest of the fight and one draw
|
||
decides the whole engagement.
|
||
3. **The engine's own description says otherwise** — "current printed base + one
|
||
die" per round, and load, wounds and coherence all change mid-fight without ever
|
||
being re-read.
|
||
|
||
Seen in the fight above: the order was identical for all seven rounds, and the
|
||
marksman that Ceyla out-ran in round 1 stayed out-run until it died.
|
||
|
||
Not yet fixed — it changes every fight and is the owner's call.
|
||
|
||
`R-152` **Not a bug: the Major Wound d10 is uniform.** Three catastrophic results in
|
||
one fight looked rigged. 4,000 draws give every row 9.3–11.0%. Variance, not a
|
||
weighted table.
|
||
|
||
`R-153` **Initiative re-rolls every round — R-151 closed.** `RingBRPCombat.nextRound()`
|
||
now resets and re-rolls after `super.nextRound()`. Six consecutive rounds gave six
|
||
distinct orders; before the change, four rounds gave the same four numbers.
|
||
|
||
```
|
||
r1: Ceyla 28.6 Dunn 21 Tomas 20 Rakhi 16
|
||
r2: Ceyla 25.6 Dunn 22 Tomas 19 Rakhi 19
|
||
r3: Ceyla 25.6 Dunn 23 Tomas 21 Rakhi 16
|
||
r4: Ceyla 27.6 Tomas 24 Dunn 22 Rakhi 19
|
||
r5: Ceyla 27.6 Dunn 22 Tomas 19 Rakhi 18
|
||
r6: Ceyla 28.6 Tomas 24 Dunn 21 Rakhi 15
|
||
```
|
||
|
||
Three consequences deliberately handled rather than left to fall out:
|
||
|
||
**Aim is spent, not permanent.** Aim is read by the roll it paid for and then
|
||
cleared, so "aim one round, fire the next" costs a round and buys one round. Left
|
||
alone it would have granted +2 for the rest of the fight the moment re-rolling made
|
||
it matter. Verified: mod 2 applied on the next round, flag 0 after, mod 0 the round
|
||
following. Edge and Drag persist — they describe equipment and conditions, which do
|
||
not evaporate at a round boundary.
|
||
|
||
**The downed are not re-rolled.** Only combatants above 0 hit points and not
|
||
unconscious draw a die; the rest keep a null initiative, which the existing
|
||
comparator already sorts to the bottom. Before this, a corpse drew a d6 every round
|
||
and could sort above the living. Verified: Rakhi bleeds to 0 in round 3 and holds
|
||
"—" from then on.
|
||
|
||
**The re-roll runs after blood loss**, so an agent who bleeds out in the round
|
||
transition does not receive a fresh initiative first.
|
||
|
||
**Mid-fight changes now reach the order at last.** Rakhi dropping her pack moved her
|
||
printed value 13 → 15 and her very next roll used 15. Under frozen initiative that
|
||
decision — the one the whole load rule exists to create — could not affect the fight
|
||
it was made in.
|
||
|
||
Played through: order churned every round (the marksman ran 24 → 22 → 24 → 19,
|
||
Dunn and Tomas swapping repeatedly), while Ceyla still led all five rounds. That is
|
||
`R-138` visible in play, not a fault in the re-roll: at laneMultiplier 3 she leads
|
||
94.8% of rounds. Re-rolling changes the *middle* of the order every round and leaves
|
||
the top almost fixed.
|
||
|
||
Also seen: Dunn's manipulation Major Wound took his knife from 34% to **4%**, and it
|
||
stayed there for the rest of the fight.
|
||
|
||
# Round 19 — Strike Rank residue, and the missing damage roll. 9 Aug 2026
|
||
|
||
`R-154` **Strike Rank survived in three talents and four dead lang keys.** The
|
||
mechanic was removed in round 12; its vocabulary was not. Found by sweeping code,
|
||
templates, language, packs and the live world:
|
||
|
||
| | was | now |
|
||
|---|---|---|
|
||
| First Through | "declare an action at Strike Rank 1 once per fight" | "act first in the round no matter what anyone rolled" |
|
||
| Out of Step | "act as though your Strike Rank were 1 lower" | "+3 Reaction for that round — a full lane" |
|
||
| Quorum | "one additional non-combat action, at Strike Rank +2" | "resolved 2 points lower on the Reaction order" |
|
||
|
||
**Four language keys were referenced by live code and did not exist.**
|
||
`RINGBRP.SR.ReadinessSkill` and `RINGBRP.SR.ReadinessInt` are read every time a
|
||
character sheet renders, so the sheet was printing the raw key string. Renamed to
|
||
`RINGBRP.React.*` and supplied ("from training" / "from wits").
|
||
`RINGBRP.SR.DeclareHint` and `RINGBRP.SR.Action` belonged to
|
||
`templates/dialog-declare.hbs`, the old declare-your-action dialog, which nothing
|
||
has opened since Strike Rank was removed. Template deleted.
|
||
|
||
Two mentions remain deliberately: the comment above `rollReaction` and a README
|
||
row, both of which say Reaction Lanes *replaced* Strike Rank. Those are history,
|
||
not rules.
|
||
|
||
**A note on the search.** My first pack scan reported zero hits because
|
||
`ClassicLevel` could not open the databases while Foundry held the lock, and the
|
||
`try/catch` swallowed it — the same shape as `R-98`, a check that reports success
|
||
when it has not run. Re-run against the live world, it found eight.
|
||
|
||
`R-155` **A successful attack did not offer its damage.** The roll ended at
|
||
"Success" and left the player to find the weapon on the sheet and click it
|
||
separately — and that path did not know what had been rolled, so **a critical did
|
||
ordinary damage**.
|
||
|
||
Fixed at the cause, which was again duplication: `_onRollDamage` existed **twice**,
|
||
once on the character sheet and once on the item sheet, near-identical and neither
|
||
aware of the success level. Both now delegate to one `rollWeaponDamage({ actor,
|
||
weapon, level, band })`, and successful attack cards carry a **Roll damage** button
|
||
bound to it.
|
||
|
||
Success levels now reach the dice (spec §4.4):
|
||
|
||
```
|
||
critical → 6, 6, 6, 6, 6, 6 maximum weapon damage, armour ignored
|
||
success → 6, 4, 5, 3, 3, 5 normal 1d4+2, full armour
|
||
```
|
||
|
||
The damage modifier is still *rolled* on a critical rather than maximised, per the
|
||
rule. The card carries the note — "maximum damage, armour ignored" for a critical,
|
||
"armour counts half" for a special — so the level is not lost between the two rolls.
|
||
The button disables itself once used, so one hit buys one damage roll, and it is
|
||
bound on render rather than inline so it still works after a reload.
|
||
|
||
Verified live: button present on critical, special and success; absent on failure
|
||
and fumble; clicking produces "Utility knife — damage — armour counts half", total 6.
|
||
|
||
`R-156` **Kit dropped onto a character was not readied.** The catalogue packs ship
|
||
armour and weapons with `equipped: false` — correct for a compendium entry, wrong
|
||
the moment one lands on an actor. A vest dragged onto a sheet contributed **0
|
||
armour** and a pistol never reached the READIED strip. Nothing errored: the item was
|
||
there, the number simply did not move, which is the hardest kind of defect to notice.
|
||
|
||
Fixed with a `preCreateItem` hook. **Only the first of each kind is readied** —
|
||
a spare vest or a second pistol stays stowed, because silently stacking armour points
|
||
or crowding the strip is the opposite failure. Anything arriving explicitly equipped
|
||
(pregens, generated characters) is untouched.
|
||
|
||
| dropped | result |
|
||
|---|---|
|
||
| 1st armour | worn, armourPoints 6 |
|
||
| 2nd armour | stowed, armourPoints stays 6 |
|
||
| 1st weapon | readied |
|
||
| 2nd weapon | stowed |
|
||
|
||
`R-157` **"ARMOUR 0" now says why.** Carrying armour you have not put on displayed
|
||
as a bare zero beside a list containing a vest, which reads as a broken sheet rather
|
||
than a state of the fiction. The box now turns red and prints **NOT WORN**, with a
|
||
tooltip pointing at the shield toggle on the Gear tab. Verified rendering as
|
||
"ARMOUR 0 NOT WORN".
|
||
|
||
This was the reported symptom. The underlying cause was `R-156`; the display change
|
||
is so that if it ever happens again — a player unequips deliberately — the sheet
|
||
says so instead of looking wrong.
|
||
|
||
`R-158` **`generateCharacter` threw on an unknown role.** `ROLES[role]` came back
|
||
undefined and the next line read `R.stat`, so a typo in a macro produced a stack
|
||
trace instead of a character. It now falls back to a random valid role, the way
|
||
`tier` already did. Found while testing the armour paths, not by design.
|
||
|
||
Existing actors were checked and are unaffected: Ceyla 6, Dunn 3, Rakhi 7, Tomas 2.
|
||
|
||
# Round 20 — legibility. 9 Aug 2026
|
||
|
||
`R-159` **Installed items now file themselves into folders.** `installContent` put
|
||
125 items into one flat alphabetical column, where a Service pistol sits between
|
||
Science (Physics) and Sleight of Hand and nothing can be found. One folder per pack:
|
||
Skills 52 · Talents 18 · Weapons 18 · Armour 10 · Gear & Devices 27, unfiled 0.
|
||
Folders are matched by name on re-install so running it twice does not produce
|
||
"Weapons (2)", and items already installed flat are swept into place, so an existing
|
||
world tidies itself rather than keeping two conventions at once.
|
||
|
||
`R-160` **The Reaction number now shows its working.** "23.6" on its own is
|
||
unreadable — it does not say whether the agent is quick or merely alert, nor what
|
||
the kit is costing. A collapsible breakdown sits under the rail:
|
||
|
||
```
|
||
Reflex DEX 10 ÷ 3, rounded up 4
|
||
Awareness Readiness 71 ÷ 10, rounded up 8
|
||
Readiness comes from training: your best of Tradecraft,
|
||
Insight, Spot or Listen (71) — wits would only give 51
|
||
Lane the better of the two × 3 24
|
||
Load 15.4 / 12 kg — 3.4 kg over −0.4
|
||
Reaction on the sheet — then add d6 23.6
|
||
```
|
||
|
||
The Readiness line is the part that was genuinely invisible: it is the best of
|
||
**Tradecraft, Insight, Spot or Listen**, floored at **INT × 3** so a clever but
|
||
untrained agent is never flat. Ceyla is an analyst — DEX 10 gives her Reflex 4, but
|
||
Tradecraft 71 gives her Awareness 8, and the better of the two sets her lane. That
|
||
is the design working as intended: she is first in the order because she is alert,
|
||
not because she is fast.
|
||
|
||
Put in the reaction area rather than the character's notes, deliberately — notes are
|
||
free text and would go stale the moment a skill or a pack changed, which is the
|
||
duplication defect this project keeps re-learning.
|
||
|
||
`R-161` **"Error lance" renamed.** The name came from the setting premise — a
|
||
crossing is an induced filing error — but it reads as a bug rather than a weapon.
|
||
Renamed with its sibling, since "Error cutter" had the same problem and leaving one
|
||
would have broken the pair:
|
||
|
||
| was | now |
|
||
|---|---|
|
||
| Error lance | **Redaction lance** |
|
||
| Error cutter | **Excision cutter** |
|
||
|
||
Both keep the bureaucratic-horror register the setting asks for — the Custodians
|
||
correct the record — without naming a weapon after a fault condition. The journal
|
||
text was updated with them.
|
||
|
||
# Round 21 — the sheet as a thing you read. 9 Aug 2026
|
||
|
||
`R-162` **The species drop-down is gone from Anatomy.** Every character sheet
|
||
offered a choice of all three anatomies, which is not a decision anyone makes while
|
||
playing — a Baseline is not one option among three, it is what the character is. The
|
||
tab now names the species and shows only its locations. The selector moved to Bio,
|
||
where identity lives, with a note that changing it changes the anatomy.
|
||
|
||
`R-163` **A figure, drawn per species, on the Combat tab.** `RINGBRP.bodyArt` gives
|
||
each species profile a shape per location, keyed by the same ids as
|
||
`RINGBRP.locationTables` — so the drawing and the table are the same data and a
|
||
location cannot appear in one without the other. A species with no art simply draws
|
||
nothing rather than drawing a human by mistake.
|
||
|
||
The anatomy is honest rather than generic: **Baseline** is a seven-part humanoid;
|
||
**Vesh** is asymmetric with one long grasping limb, one short bracing limb, a leaning
|
||
trunk and a sensory ridge where a head is not; **Cadence** is five separate bodies,
|
||
equal and unranked, which is the point of them.
|
||
|
||
Five states, readable at sheet size without reading a number:
|
||
whole · grazed (any damage) · hurt (half) · disabled · destroyed (dashed outline).
|
||
Hovering a limb highlights its table row and the reverse, so the picture and the
|
||
numbers are visibly the same thing.
|
||
|
||
`R-164` **The location table was ragged because every column sized itself.** Now
|
||
`table-layout: fixed` with a flexible name column, fixed 3.6em numeric columns,
|
||
right alignment and `font-variant-numeric: tabular-nums`, so digits line up down the
|
||
column instead of drifting with content width.
|
||
|
||
`R-165` **Wounds moved onto the Combat tab**, beside the figure, with the full
|
||
effect text and the clear button. They remain on their own tab as well — nothing was
|
||
taken away, the information was simply put where it is used.
|
||
|
||
`R-166` **A wounded agent is now visible on the first tab.** Major Wounds modify
|
||
every roll on the Skills tab and were legible only two tabs away. A red bar sits
|
||
above the skill list naming each wound and the arithmetic it is applying:
|
||
|
||
```
|
||
WOUNDED [Concussed] [Bleeding] all rolls -20% · Manipulation -30% · bleeding 1/round
|
||
```
|
||
|
||
It only appears when there is something to say.
|
||
|
||
Verified live across all three species: 7 / 6 / 5 parts drawn, states resolving
|
||
correctly against real location damage, drop-down absent from Anatomy and present on
|
||
Bio, wound bar rendering with the right numbers.
|
||
|
||
# Round 22 — the rules go in the box. 9–10 Aug 2026
|
||
|
||
`R-167` **A rules journal, generated from the rules.** `packs/rules` ships
|
||
"The Custodians — Rules", seven pages, built by `tools/rules-text.mjs` from
|
||
`rules.mjs` and `lang/en.json`. **No number in the book is typed by hand.** Hit
|
||
points, Major Wound thresholds, the damage ladder, the reaction formulas, the wound
|
||
table and its consequences, coherence bands and costs, the bleed rates, the range
|
||
ladder and the encumbrance table are all computed at build time; every wound and band
|
||
name is read from the language file, so the book and the interface cannot disagree.
|
||
That defect has landed five times in this project — a hand-written rulebook would
|
||
have been the sixth.
|
||
|
||
Shaped to be learned and then abandoned:
|
||
|
||
| Page | |
|
||
|---|---|
|
||
| 1 · The whole game | the d100 core, the three modifiers, where the numbers come from |
|
||
| 2 · Fighting | reaction, the turn, range, what a hit does |
|
||
| 3 · Getting hurt | locations, the Major Wound table, bleeding |
|
||
| 4 · Coherence | the setting mechanic |
|
||
| 5 · A case | the session structure |
|
||
| 6 · Reference | tables you look up, never read |
|
||
| 7 · A round, worked through | Ceyla's real sheet numbers, start to finish |
|
||
|
||
Page 1 says explicitly that it is enough to play, and page 7 walks one round using
|
||
figures a reader can check against the shipped pregen.
|
||
|
||
`R-168` **Four more rule tables moved into `rules.mjs`** so the journal could be
|
||
generated from them at all: the circumstance ladder, the range ladder, the Major
|
||
Wound table and the coherence bands, costs and recovery. `ringbrp.mjs` now assigns
|
||
rather than re-declares them, `rangeBandFor` and `coherenceBand` are aliases of the
|
||
shared functions, and the guard covers all of it — 17 rules, 8 constant sets, 54
|
||
formulas.
|
||
|
||
`R-169` **The printed chance was not the real chance.** `resolveBands` returned a raw
|
||
`effective`, so a skill beaten below zero printed **0%** on the chat card. The floor
|
||
was genuinely there — `gradeRoll` tests the critical band first, so a 1 still
|
||
crits — but it arrived sideways, and the card contradicted the rule. `effective` is
|
||
now the clamped figure. Verified: rating 1 at Difficult prints 1% and succeeds 1% of
|
||
the time; every band from 1 to 100 now prints what it actually does.
|
||
|
||
`R-170` **The 96–00 rule was in the engine and in no document.** `gradeRoll` has
|
||
always failed 96–99 regardless of skill and fumbled on 00 — correct BRP, and the
|
||
reason a 100% skill really succeeds 95% of the time. It appears nowhere in the design
|
||
notes and would have appeared nowhere in the book. Now on page 1 and in the reference.
|
||
|
||
`R-171` **Correction: my combat harnesses double-counted armour.** `hitLocationRoll`
|
||
applies armour itself and takes an `armourApplied` flag for callers that have already
|
||
done it. My harnesses deducted armour and did not pass the flag, so every playtest
|
||
from round 16 onward understated damage. The engine was right throughout.
|
||
|
||
Re-run with armour counted once, the same fight resolves in **4 rounds instead of
|
||
7–12**, and Tomas goes down in round one. Combat is materially more decisive than my
|
||
earlier reports suggested — worth knowing before any further balance work.
|
||
|
||
**Sweeps run this round.** Generators across every role, tier and species (85 checks);
|
||
subsystems — wounds, bleeding, healing, coherence, skill rolls, encumbrance edges,
|
||
sheets, tables, every pack (25 checks); talents and progression (6 checks). Everything
|
||
that failed was my test's assumption rather than the system, except `R-169`, which was
|
||
real. `check-rules` is green and gates the build.
|
||
|
||
# Round 23 — the table, and a species that does not exist. 10 Aug 2026
|
||
|
||
`R-172` **The hit-location table was unreadable, and `table-layout: fixed` was why.**
|
||
The header read `MELEERANGETAKENARMO`, ranges wrapped onto two lines, rows were
|
||
different heights and the numbers sat a hand's width from the names they described.
|
||
|
||
Three separate causes, all mine:
|
||
|
||
1. **Fixed layout sizes every column from the first row** — the header row, where
|
||
the font is 0.62rem. `width: 3.6em` therefore resolved to about 39px, too narrow
|
||
for "RANGED", so the headers overflowed into each other and the ranges wrapped.
|
||
2. **`.rb-loc-cell` was `display: flex` on a `<td>`**, which takes the cell out of
|
||
table layout altogether — it had been there since before these columns existed.
|
||
3. **The name column was given `width: 99%`**, so on a wide sheet it absorbed 352px
|
||
of slack and threw the numbers to the far edge of the panel.
|
||
|
||
Now: auto layout, `white-space: nowrap`, content-sized columns, a fixed 22px gutter
|
||
after the name, and `width: auto` so the table is only as wide as it needs to be.
|
||
Measured in isolation at four container widths — 1400, 900, 640 and 420px — the
|
||
table is **297px every time**, every row **20px**, the last column right-aligned to
|
||
within a pixel, across Baseline, Vesh, Cadence and a wounded sheet.
|
||
|
||
Also: the roll button carried a whole sentence and wrapped to three lines — short
|
||
label, sentence moved to the tooltip — and "No active wounds." was centred in a wide
|
||
empty box instead of sitting under its heading.
|
||
|
||
**A note on verification.** Foundry reports its window as 0×0 in this browser, so
|
||
layout cannot be measured in place. The check was done by lifting the real markup
|
||
and the real stylesheet into a shadow root at controlled widths — which measures the
|
||
shipped CSS rather than a mock-up of it.
|
||
|
||
`R-173` **Every packed actor was a species the game does not define.** Ten of them —
|
||
all four pregens and all six NPCs — shipped with `speciesProfile: "hominid"`, a name
|
||
left from the Ringworld setting. It is not a key in `locationTables`, so the sheet
|
||
fell back to Baseline and nobody noticed: the hit locations were right by accident,
|
||
and would have been silently wrong the moment a Vesh or a Cadence was built from a
|
||
pack.
|
||
|
||
Found by accident while restoring test actors, which is not a system I want to rely
|
||
on. Fixed in three places:
|
||
|
||
- `template.json`'s default is `baseline`, not `hominid`;
|
||
- `buildActor` now **writes the profile explicitly** from the content spec rather
|
||
than leaning on the template default, so a packed actor always carries a real one;
|
||
- `check-rules` fails the build if any `speciesProfile` in `template.json` is not a
|
||
key of `locationTables`. Verified by reintroducing "hominid": the build stops with
|
||
`speciesProfile "hominid" is not a species (have: baseline, vesh, cadence)`.
|
||
|
||
The actor packs were deleted and rebuilt from clean, since the stale value was baked
|
||
into the stored documents rather than coming from anything still in the source.
|
||
Verified: ten actors, zero invalid profiles.
|
||
|
||
# Round 24 — a fight, and what the sheet knew about it. 10 Aug 2026
|
||
|
||
Four agents against a marksman and two contractors, 25 m closing to contact. Real
|
||
`Combat` document, armour counted once, initiative re-rolled each round. Agents took
|
||
it in four rounds, three standing; Dunn went down in round three.
|
||
|
||
The sheet was then read against the fight log, and it agreed on everything it
|
||
covered — Ceyla's left leg at 4/6 showing `hurt` on the figure, Dunn's head at 13/5
|
||
showing `destroyed`, his bleed on the wound bar, armour, reaction, MOV. What it
|
||
covered was the problem.
|
||
|
||
`R-174` **Only legs and arms ever did anything.** The location-effect table is
|
||
localised prose for nine anatomical kinds and the derived state implemented two of
|
||
them. Three of the remaining seven were live rules doing nothing:
|
||
|
||
| Kind | The text promises | It applied |
|
||
|---|---|---|
|
||
| `head` | "Unconscious immediately" | prone, and nothing else — **the agent stayed up** |
|
||
| `ridge` (Vesh) | "−30% to all Perception" | **nothing** |
|
||
| `unit` (Cadence) | "each disabled body costs the whole person −10% to everything" | **nothing at all** |
|
||
|
||
The Cadence one is the serious one: losing bodies is that species' entire mechanical
|
||
identity, and it was costing them nothing. A Cadence was strictly better than a
|
||
Baseline — same hit points spread over five bodies with no vital location to hit and
|
||
no penalty for losing four of them.
|
||
|
||
Found by shooting Dunn in the head and noticing he was still standing.
|
||
|
||
**Fixed the same way the Major Wounds were.** `locationEffectsFor(locations)` in
|
||
`rules.mjs` turns each kind into numbers; `prepareDerivedData` folds it together with
|
||
`woundEffectsFor` rather than computing anything inline. The two stack, because a
|
||
shattered leg and a concussion are separate facts about the same body.
|
||
|
||
Verified live:
|
||
|
||
```
|
||
leg destroyed phys -30 MOV 1
|
||
arm destroyed manip -30
|
||
HEAD destroyed unconscious=true down=true
|
||
vesh RIDGE opened percep -30
|
||
cadence 1 body lost all -10
|
||
cadence 3 bodies lost all -30 → a 60% skill rolls at 30%
|
||
leg gone + concussed all -20, phys -30, MOV 1 ← both tables stacking
|
||
```
|
||
|
||
Guard extended to 18 rules and 63 formulas, including every location kind the three
|
||
species tables use, so a kind cannot be added to a species without a consequence.
|
||
|
||
**Still prose, deliberately:** `vital` says "knocked down, and a Stamina roll or
|
||
unconscious" — the knockdown applies, the Stamina roll is left to the GM, which is
|
||
the right place for it.
|
||
|
||
# Round 25 — a mixed team, and the banner that was only half looking. 10 Aug 2026
|
||
|
||
Ceyla (Baseline), a generated Vesh and a generated Cadence against three Cleaners,
|
||
25 m closing to contact. Chosen so the location effects wired in round 24 would be
|
||
exercised by real dice rather than by me setting fields.
|
||
|
||
They were. The Cadence took a body and the sheet reported
|
||
`all-10 · 1 body lost`; a Cleaner's arm was disabled and his next shot went out at
|
||
**5%** — 45 less 30 for the arm, less 10 for range. A marksman's leg went and he
|
||
finished the fight at MOV 5 with `phys -30`. Team won in three rounds without a
|
||
casualty.
|
||
|
||
`R-175` **The Skills-tab banner was only watching one of the two sources.** It keyed
|
||
off `wounds.length`, which counts Major Wounds. Lost locations now produce identical
|
||
penalties and were invisible on that tab:
|
||
|
||
- a Cadence down two bodies sat at **−20% to everything** and the skills page said
|
||
nothing;
|
||
- a Vesh with an opened ridge and a dead grasp limb was at **−30% Manipulation and
|
||
−30% Perception**, likewise silent.
|
||
|
||
This is precisely the defect the banner was built to fix, for the other half of the
|
||
system. It now reads from a single `impaired` context built from the derived state,
|
||
so it shows whatever is actually modifying rolls, whatever produced it:
|
||
|
||
```
|
||
Cadence, 2 bodies lost
|
||
WOUNDED [First body ✕] [Fifth body ✕] all rolls -20%
|
||
Vesh, ridge destroyed and grasp limb gone
|
||
WOUNDED [Grasp limb] [Sensory ridge ✕] Manipulation -30% · Perception -30%
|
||
Cadence, bodies lost AND concussed and bleeding
|
||
WOUNDED [Concussed] [Bleeding] [First body ✕] [Fifth body ✕]
|
||
all rolls -40% · bleeding 1/round
|
||
Ceyla, head destroyed
|
||
UNCONSCIOUS [Head ✕]
|
||
Ceyla, unhurt
|
||
(no banner)
|
||
```
|
||
|
||
The headline changes with the state — **Wounded**, **Out of the fight**, or
|
||
**Unconscious** — so the worst news is the first word rather than something to be
|
||
inferred from a list of chips.
|
||
|
||
# Round 26 — Coherence in a fight, and a rule that had never once applied. 10 Aug 2026
|
||
|
||
Four agents against a Revenant, a marksman and a contractor, opening at 40 m so the
|
||
range ladder had work to do. Built around the two things dice had never exercised:
|
||
Ceyla firing the Redaction lance, which costs Coherence a shot, and Rakhi spending a
|
||
round aiming.
|
||
|
||
Both worked. Aim paid out — `Rakhi 21(+2)` in round two, one round after she spent
|
||
her action. Coherence ran **10 → 5** across five shots and left Ceyla **Loose** at
|
||
the end of the fight, which is the design's whole argument: the tool that solves the
|
||
case is the tool that costs you. And the stacking held up — the marksman took a
|
||
disabled arm and then a manipulation Major Wound and finished the fight shooting at
|
||
**1%**: 55, less 10 for range, less 60 for two −30s.
|
||
|
||
`R-176` **Encumbrance's skill penalty had never been applied to anything.**
|
||
`encumbranceFrom` has returned a `skillPenalty` since R-23. `prepareDerivedData`
|
||
stored it, the sheet displayed it, the rules journal tabulated it, and no roll ever
|
||
read it. Rakhi has been 20 kg over her free carry all session, carrying a printed
|
||
−10%, and rolling at full.
|
||
|
||
The comment above the modifier chain in `skillRoll` says gear bonuses "sit outside
|
||
the ±40% circumstance cap **alongside encumbrance and coherence**" — so it was
|
||
always meant to be there. Coherence was wired; encumbrance was not.
|
||
|
||
Now applied at the same choke point. **Scoped to physical, melee, ranged and
|
||
manipulation** — a heavy pack does not make you worse at Knowledge, and a rule that
|
||
says otherwise is one nobody will use at the table. That is a decision rather than a
|
||
fix, and the rules journal now states the scope explicitly so book and code agree.
|
||
|
||
Verified: Rakhi's rifle 47% → **37%** with the card reading `Overloaded: -10%`, and
|
||
her Navigate untouched at 31%.
|
||
|
||
`R-177` **The banner was watching two of four sources.** Rounds 24 and 25 taught this
|
||
lesson twice; here it was a third time. Four things modify rolls through
|
||
`skillRoll` — Major Wounds, lost locations, Coherence and load — and the banner knew
|
||
about the first two. Ceyla at Coherence 5 was losing 5% Communication with nothing on
|
||
the Skills tab; Rakhi was overloaded and silent.
|
||
|
||
All four now feed one `impaired` context, and **totals sum per category** rather than
|
||
listing each source, so a concussion and an Unmoored band read as one figure:
|
||
|
||
```
|
||
Rakhi, overloaded
|
||
WOUNDED [Overloaded] Physical -10% · MOV 8
|
||
Ceyla, Unmoored + concussed + arm destroyed
|
||
WOUNDED [Concussed] [Right arm ✕] [Unmoored]
|
||
all rolls -30% · Manipulation -30% · Communication -20%
|
||
```
|
||
|
||
Before the fix that last line read `all rolls -20% · all rolls -10%`.
|
||
|
||
**The pattern, stated plainly.** Four rounds running, the defect has not been a wrong
|
||
rule but an incomplete reader of one: Major Wounds computed and unapplied, locations
|
||
applied for two kinds of nine, the banner watching one source then two, encumbrance
|
||
computed and never read. The guard now covers 19 rules and 69 formulas, but it checks
|
||
*formulas*, not whether anything consumes them. **A rule that is computed and never
|
||
read is the failure mode this project actually has**, and it is worth a dedicated
|
||
check: every value `prepareDerivedData` produces should have at least one reader.
|
||
|
||
# Round 27 — the callout boxes, the range picker, and what a strong person can lift. 10 Aug 2026
|
||
|
||
`R-178` **Dialogs looked like a different application.** Every callout — attack
|
||
options, skill rolls, the reaction rail, the generators — rendered in Foundry's
|
||
default parchment with a serif face, sitting on top of a dark blue sheet in
|
||
Michroma and Helvetica. `rbDialogClass()` now returns a themed subclass that tags
|
||
every system dialog with `ringbrp-window`, and that class was added to the selector
|
||
that defines the design tokens, so dialogs inherit the sheet's palette rather than
|
||
carrying a copy of it.
|
||
|
||
Verified by computed style, not by eye:
|
||
|
||
```
|
||
window bg rgb(16, 26, 110) — identical to the sheet's --rb-deep
|
||
header rgb(12, 18, 91), yellow text, Michroma
|
||
content Helvetica Neue, --rb-white on --rb-deep
|
||
select --rb-panel ground, --rb-line border
|
||
Roll button --rb-yellow on --rb-void (yellow means the thing that commits)
|
||
labels uppercase 10px in --rb-muted
|
||
```
|
||
|
||
`R-179` **The range field asked for arithmetic the weapon had already done.** The
|
||
dialog printed "Bands: 20 / 60 / 180 m" and then a number box, so the player read
|
||
the bands, decided which one they were in, and typed a distance for the system to
|
||
convert back into a band. It is now a dropdown of the bands themselves:
|
||
|
||
```
|
||
Short 1–20 m +0%
|
||
Medium 21–60 m -10%
|
||
Long 61–180 m -30%
|
||
Extreme 181–360 m -50%
|
||
Beyond range over 360 m out of range
|
||
```
|
||
|
||
Each option carries a distance inside its band rather than a modifier, so
|
||
`rangeBandFrom` remains the single authority on what a range costs — the dropdown
|
||
chooses a distance, it does not duplicate the ladder.
|
||
|
||
`R-180` **Free carry ignored how big you are.** It was STR in kilogrammes. Rakhi at
|
||
STR 18 and MAS 19 — a large, powerful woman — had a free carry of **18 kg** and was
|
||
twenty kilos over while carrying 38 kg, which is an ordinary soldier's fighting
|
||
load. A rule that treats a realistic pack as an emergency is one the table quietly
|
||
drops.
|
||
|
||
Now **STR + MAS**, the same pair that sets the damage modifier, so a single physical
|
||
axis decides both how hard you hit and how much you can shoulder.
|
||
|
||
| | STR + MAS | free | carrying | over, before → after |
|
||
|---|---:|---:|---:|---|
|
||
| Ceyla | 12 + 13 | 25 | 15.4 | 3.4 → **0** |
|
||
| Dunn | 9 + 10 | 19 | 8.2 | 0 → 0 |
|
||
| Rakhi | 18 + 19 | **37** | 38 | 20.0 → **1.0** |
|
||
| Tomas | 13 + 12 | 25 | 7.2 | 0 → 0 |
|
||
|
||
Rakhi keeps a cost for her load — 1 kg over is −0.1 Reaction, so she prints 14.9
|
||
rather than 15 — which is the granular rule doing exactly what it was written for:
|
||
a heavy pack is felt, it is not a catastrophe. Rejected `(STR+MAS)×1.5`, which gave
|
||
her 56 kg and would have made encumbrance unreachable, and `STR×2`, which ignores
|
||
mass — the thing the owner specifically asked to count.
|
||
|
||
The rules journal states the new formula, and the guard covers it.
|
||
|
||
# Round 28 — the subsystem that was only ever bookkeeping. 10 Aug 2026
|
||
|
||
`R-181` **Defending did not exist.** `system.combat.defencesThisRound`,
|
||
`defenceTypeLock` and `secondAttackUsed` are declared in `template.json`, written
|
||
into every packed actor by `buildActor`, and reset every round by the combat hook.
|
||
Nothing ever incremented them, nothing rolled a defence, and no control existed
|
||
anywhere in the interface. The rules journal — which I generated — promised "one
|
||
offensive action, plus **as many defensive reactions as you like at an escalating
|
||
penalty**".
|
||
|
||
So the escalation was the only part of the rule that had ever been built, and it was
|
||
being applied to a counter nothing could raise. This is the same failure as the last
|
||
four rounds at its largest scale yet: a whole combat subsystem present as
|
||
bookkeeping for an event that could not happen.
|
||
|
||
**Now implemented.** `defencePenaltyFor` and `defenceTypeAllowed` in `rules.mjs`;
|
||
`defend(actor, {type, weapon})` rolls through the same `skillRoll` choke point, so a
|
||
defence is subject to wounds, coherence and load like everything else; a Dodge /
|
||
Parry control on the Combat tab that shows what the next one will cost.
|
||
|
||
| Defence this round | Modifier |
|
||
|---|---:|
|
||
| 1st | free |
|
||
| 2nd | −30% |
|
||
| 3rd | −60% |
|
||
| 4th | −90% |
|
||
|
||
Verified end to end — four attacks on Dunn in one round:
|
||
|
||
```
|
||
defence 1: no penalty → 49% failure
|
||
defence 2: -30% → 19% failure
|
||
defence 3: -60% → 1% failure (the 1% floor holding)
|
||
defence 4: -90% → 1% failure
|
||
asked to parry after dodging → "dodge" (the type lock)
|
||
round tick → 0 defences, lock cleared
|
||
sheet: "DEFEND DODGE PARRY first is free"
|
||
→ "DEFEND DODGE PARRY 1 this round · next at -30%"
|
||
```
|
||
|
||
**Played.** Four agents against three contractors at 10 m closing to contact, with
|
||
every agent defending and NPCs taking only their free defence. Agents won in seven
|
||
rounds **without a casualty** — considerably softer than the same fight without
|
||
defences, which is the point: a defence is free the first time and the rule makes
|
||
standing in front of three people possible and stupid.
|
||
|
||
The escalation never fired in that fight, because with seven combatants nobody was
|
||
attacked twice in a round. It needed a forced test to exercise, which is worth
|
||
remembering — **a rule that only bites in a corner will not be exercised by ordinary
|
||
play, and that is exactly how the last five defects survived.**
|
||
|
||
The journal now documents defending, generated from `defencePenaltyFor` so the table
|
||
in the book cannot drift from the ladder in the code. Guard at 22 rules and 77
|
||
formulas.
|
||
|
||
# Round 29 — weight on the kit, and a dice set that reports the drift. 10 Aug 2026
|
||
|
||
`R-182` **Weapons and armour did not show their weight.** Gear did; the two
|
||
categories that account for most of a load did not — so a player deciding what to
|
||
drop could see that a notebook weighs 0.3 kg and not that the rifle weighs 3.4.
|
||
Weapons gained a WEIGHT column, armour gained the figure beside its points, both
|
||
right-aligned with tabular figures so a kit list scans:
|
||
|
||
```
|
||
WEAPON | DAMAGE | RANGE | AMMO | WEIGHT
|
||
Marksman rifle | 1d10+2 | 20/60/180 m | 8/10 | 3.4 kg
|
||
Entry bar | 1d10 | — | — | 3 kg
|
||
Breaching suit 7 pts 11 kg
|
||
```
|
||
|
||
That matters more now that free carry is STR + MAS and the encumbrance penalty
|
||
actually applies: Rakhi is 1 kg over 37, and the sheet finally shows her which
|
||
kilogramme to put down.
|
||
|
||
`R-183` **A dice set for Dice So Nice, and it is the setting rather than a skin.**
|
||
|
||
The agency keeps a filing system and the game is about that filing system being
|
||
wrong. So the dice carry a ruled registry grid — and **the colourset is chosen per
|
||
roll from the roller's Coherence**, so the dice report the one number the players
|
||
are trying not to look at:
|
||
|
||
| Coherence | Set | What the die looks like |
|
||
|---|---|---|
|
||
| 10–8 | Anchored | true rule, lemon numerals on ultramarine, metal |
|
||
| 7–5 | Loose | the same grid, dimmer, plastic |
|
||
| 4–3 | Adrift | the rule has begun to wander and grain shows through |
|
||
| 2–1 | Unmoored | torn registry, coral numerals, glass |
|
||
| 0 | Displaced | no grid at all — fragments of rule going nowhere |
|
||
|
||
Plus an **Agency issue** set in brushed plate for anything that is not a person.
|
||
|
||
Four textures, generated rather than drawn, by `tools/make-dice-textures.mjs` —
|
||
raw RGBA encoded to PNG through `node:zlib` with a small built-in encoder, so the
|
||
repo gains no image dependency and a rebuild is byte-identical. All four tile
|
||
cleanly, which matters on a d20.
|
||
|
||
Verified positively rather than by absence of error, since Dice So Nice falls back
|
||
silently on an unknown colourset: `dice3d.exports.TEXTURELIST` holds all four
|
||
textures, `dice3d.exports.COLORSETS` all six sets, and
|
||
`getLoadedDiceSystems()` reports "The Custodians, 7 presets" — d100 and d10 labelled
|
||
as the percentile pair, plus d4/d6/d8/d12/d20. Every roll the system makes is
|
||
dressed: skills, the reaction die, damage, hit location and the Major Wound d10.
|
||
|
||
`R-184` **A deprecation warning of my own making, removed.** The damage-button
|
||
handler registered both `renderChatMessageHTML` and `renderChatMessage` for
|
||
compatibility, and v13+ warns on every single chat message when the old name is
|
||
bound. It now picks the new name when the v13 chat class is present.
|
||
|
||
# Round 30 — art, a catalogue that spans centuries, and the Quiet Arts. 10 Aug 2026
|
||
|
||
`R-185` **Icons rebuilt with depth.** A glyph was one silhouette; it is now up to
|
||
three layers — mass in near-white, **detail** in a darker tone (the grip and sight on
|
||
a pistol, the fuller on a blade, the lens housing on a scanner), and an accent in the
|
||
rim colour. The detail layer is the revision: it is what makes a flintlock read as a
|
||
flintlock rather than a pistol-shaped blob at 32 px.
|
||
|
||
**Era is carried by the tile**, so a glance at an inventory dates a thing before the
|
||
name is read: a notched corner and muted ground for antique, a cut corner and a
|
||
scanline for recovered, and **a broken rim for anomalous** — the tile does not close
|
||
on things that should not exist. 141 icons: 99 modern, 23 antique, 10 recovered,
|
||
9 anomalous.
|
||
|
||
`R-186` **The catalogue spans time.** Weapons 18 → 30, armour 10 → 13, gear 26 → 45,
|
||
filed into era sub-folders on install rather than one alphabetical column:
|
||
|
||
```
|
||
Weapons Armour Gear & Devices
|
||
└ Antique 6 └ Antique 2 └ Antique 8
|
||
└ Agency issue 21 └ Agency 10 └ Agency issue 29
|
||
└ Recovered 3 └ Recovered 1 └ Recovered 6
|
||
└ Anomalous 2
|
||
```
|
||
|
||
Flintlock, musket, crossbow, duelling sword, boarding axe, brigandine, oil lantern,
|
||
tallow candles, brass compass, pocket watch, chirurgeon's roll, wax tablet. Gauss
|
||
sidearm and lance-rifle, monofilament blade, survey drone, optic visor, data wafer,
|
||
stasis cannister, sealed hardsuit. And two that should not be in a kit list at all:
|
||
a **threshold key** that opens a crossing a hand's width, and **the quiet ledger**,
|
||
which writes down what happened — sometimes before it has.
|
||
|
||
`R-187` **THE QUIET ARTS.** The setting already contained the answer, so the powers
|
||
are not an addition to it. Crossing is an *induced filing error* — you persuade the
|
||
record you are somewhere else. **An Art is the same trick performed small and on
|
||
purpose: a misfiling committed deliberately.** There are therefore no fireballs;
|
||
every Art is a sentence about the record.
|
||
|
||
| Art | It says | Ordinary | Pushed |
|
||
|---|---|---:|---:|
|
||
| **Thread** | what was together is still together | −1 | −2 |
|
||
| **Echo** | *when* is loosely filed | −1 | −2 |
|
||
| **Hollow** | this is not filed here | −1 | −3 |
|
||
| **Witness** | the record agrees you belong | −1 | −2 |
|
||
| **Unmake** | this was not present | −2 | −4 |
|
||
| **Ledger** | read the record — and amend it | −2 | −4 |
|
||
|
||
Three design decisions worth recording:
|
||
|
||
**No new resource.** The cost is Coherence, because using an Art *is* doing something
|
||
that only makes sense somewhere else — precisely what transposed materiel already
|
||
charges for. It is levied whether or not the roll succeeds, since the misfiling is
|
||
committed the moment you try.
|
||
|
||
**Coherence penalises the Arts.** They run through the same `skillRoll` choke point
|
||
as everything else, so the further an agent has drifted the worse they are at the
|
||
thing that made them drift. Verified: Echo at 30% rolls 30 / 25 / 20 at Coherence
|
||
10 / 4 / 1, and at 0 the attempt is refused — there is nothing left to spend.
|
||
|
||
**A fumble is not a failure.** The misfiling *sticks*: something small and true about
|
||
the world is now different and the GM never says which. A story hook with a
|
||
mechanical anchor.
|
||
|
||
`R-188` **They only ever arrive from crossings**, per the owner's decision. There is
|
||
no path in but `grantPower()`: not taught, not bought, not chosen at creation, not
|
||
offered by the generator. Verified — a freshly generated character has none, and a
|
||
second grant of the same Art is refused. Each carries an `origin` line naming the
|
||
crossing that left it, printed on the sheet:
|
||
|
||
```
|
||
Echo 30% −1 [USE] [PUSH −2]
|
||
When is loosely filed.
|
||
Left by: Ilfracombe, the second crossing. She came back eleven minutes before she left.
|
||
```
|
||
|
||
New Item type `power`, its own compendium, an Arts tab, and a rules-journal page
|
||
generated from `ARTS` and `powerCostFor` so the book cannot drift. Guard at 24 rules
|
||
and 85 formulas — it caught both new rules arriving without coverage and stopped the
|
||
build until they had it.
|
||
|
||
`R-189` **The return is now a roll, and it is the only door the Arts come through.**
|
||
`returnCrossing()` described the modifiers and nothing ever resolved them — the GM
|
||
rolled it themselves, so "a crossing that goes wrong" was not an event the system
|
||
could observe. `crossBack(actor, {aimedByAnother, aimedBy})` resolves it through the
|
||
usual choke point.
|
||
|
||
| Return | What happens | Coherence | An Art? |
|
||
|---|---|---:|---|
|
||
| Success or better | home, where and when you meant | — | no |
|
||
| **Failure** | you arrive near your target, not at it | −1 | only if already Unmoored |
|
||
| **Fumble** | you come back, and not the same | −2 | **always** |
|
||
|
||
**R-56 is enforced in code rather than by convention.** There is no branch that
|
||
leaves an agent across: `setAcross(false)` runs on every outcome. Verified over
|
||
twenty returns by an Unmoored agent rolling alone — never stranded once.
|
||
|
||
**You do not choose the Art, and the pool darkens as you come apart:**
|
||
|
||
| Coherence when it went wrong | What can arrive |
|
||
|---|---|
|
||
| 8+ Anchored or Loose | Thread, Echo, Witness |
|
||
| 4–3 Adrift | Hollow, Thread, Echo |
|
||
| 2 and below Unmoored | **Unmake, Ledger, Hollow** |
|
||
|
||
Which is the trap the setting has been laying since the Coherence track was written:
|
||
**the agents most likely to come back with an Art are the ones least able to afford
|
||
using it.** Verified live — an anchored fumble produced Witness at 30%; an Unmoored
|
||
failure produced Unmake.
|
||
|
||
`R-190` **Three of the four pregens could not perform the job's defining act.**
|
||
Transposition sat at its printed base of 1% for Ceyla, Rakhi and Tomas; only Dunn had
|
||
it at 68. Every return would have failed, making "you arrive near your target" the
|
||
normal experience of being a Custodian and quietly reducing the crossing rules to
|
||
scenery.
|
||
|
||
Defensible as a design — one specialist aims for the team, and `aimedByAnother`
|
||
exists precisely for that — but not at 1%: these are people who cross for a living.
|
||
Ceyla 32, Rakhi 28, Tomas 35, Dunn unchanged at 68 as the field lead. The team can
|
||
now get itself home, and is markedly better off doing it through Dunn.
|
||
|
||
Found only because `crossBack` made the roll real. A skill nothing rolls is a skill
|
||
nobody notices is wrong — the same shape as R-176 and its four predecessors.
|
||
|
||
`R-191` **A crossing panel on the case file.** The sheet had a single `toggle-across`
|
||
flag-flip, which is right for going out — the brief names a place and a time and the
|
||
margin is the case's first problem — and wrong for coming back, because coming back
|
||
is a roll.
|
||
|
||
The panel now shows the whole decision at a glance while the team is across:
|
||
|
||
```
|
||
CROSSING [ BRING THEM HOME ]
|
||
AGENT | SKILL | COH | ALONE |
|
||
Ceyla | 32% | 10 | 32% | [RETURN] [AIMED]
|
||
Dunn | 68% | 10 | 68% | [RETURN] [AIMED]
|
||
Rakhi | 28% | 2 | 1% | [RETURN] [AIMED]
|
||
Tomas | 35% | 10 | 35% | [RETURN] [AIMED]
|
||
```
|
||
|
||
The **ALONE** column is the argument the panel exists to make: Rakhi, Unmoored at
|
||
Coherence 2, aims her own way home at **1%** — 28 less the −30 the band imposes,
|
||
floored. AIMED routes her through the best crosser at no penalty. Nobody has to know
|
||
the rule to see the cost.
|
||
|
||
`R-192` **`crossBack` no longer ends the case.** It cleared `across` on the case file,
|
||
so the first agent through the door marked the whole team home while the rest were
|
||
still standing in it. It now returns only the agent it was called for, and the caller
|
||
decides when the last one is back — which is what "Bring them home" does, looping
|
||
every member through the best aimer and then clearing the flag.
|
||
|
||
Verified end to end: Dunn aimed for the team, three came home at a cost of 1
|
||
Coherence apiece for rough returns, and **Rakhi failed hers while Unmoored and came
|
||
back with Hollow** — the dark pool, arriving exactly as designed, on the agent least
|
||
able to afford it.
|
||
|
||
# Round 31 — a full case, played end to end. 10 Aug 2026
|
||
|
||
Played as the team's **second** crossing, because Arts only arrive from crossings
|
||
that go wrong: Rakhi carried the Hollow she came back with from the first Ilfracombe
|
||
return. Brief → cross → four phases → first contact → an Art used → a fight →
|
||
return → debrief → end of case.
|
||
|
||
**What the case did.** The destination is *rumoured*, so every phase is Difficult:
|
||
Tradecraft 71 halves to 35 before the terrain modifier. Three of four phases failed
|
||
and the team reached the cove on **Containment 7, Resources 2**. Rakhi hollowed the
|
||
regulator case to get it past a watched approach — Hollow 26%, success, −1 Coherence.
|
||
Two contractors at the cove went down in two rounds. Dunn aimed everyone home at 68%
|
||
and took the only rough return himself. Debrief restored the team to Anchored.
|
||
|
||
That is the loop working: the pressure came from the tracks and the Coherence, not
|
||
from the dice being unkind.
|
||
|
||
`R-193` **First contact could not be made with a group nobody had met.**
|
||
`standingFor` returned `null` when a group had no standing entry, and
|
||
`resolveContact` bailed on null — so the *first* contact with a new group was the one
|
||
thing first contact could not do. Every stage silently returned nothing. It had gone
|
||
unnoticed because the shipped case file ships with a standing already on it; the bug
|
||
only appears with a group the team encounters for the first time, which is the normal
|
||
case in play and never once in testing.
|
||
|
||
`ensureStanding()` creates the entry on demand. Verified against a wiped standings
|
||
list: signal → success, read → failure, offer → success, standing 2 and impression 2
|
||
recorded.
|
||
|
||
`R-194` **The skills the job most uses could never improve.** The experience tick in
|
||
`skillRoll` fires only when a `skillItem` is passed. Every internal caller — case
|
||
phases, First Aid, Medicine, defences, and the crossing itself — passes a
|
||
`skillFamilyId` and no item. So Transposition, Tradecraft, Dodge and First Aid were
|
||
structurally incapable of earning a tick, and `endOfCase` reported "0 skills ticked"
|
||
for the entire team after a full case.
|
||
|
||
`skillRoll` now resolves the item from the family when the caller has not handed one
|
||
over. Verified: Ceyla ticked Dodge from a defence, Dunn ticked Transposition from a
|
||
return, and at end of case **Transposition improved 68 → 69**.
|
||
|
||
Both defects are the same shape as the run since R-176 — a rule that exists but that
|
||
nothing reaches — and both were found by *playing the case* rather than by testing a
|
||
subsystem. Two harness errors of mine on the way, recorded so they are not re-found
|
||
as bugs: the contact stages are `signal` / `read` / `offer`, not approach/pidgin; and
|
||
combat rolls driven through `resolveBands` directly bypass `skillRoll`, so they
|
||
legitimately do not tick.
|
||
|
||
**Still open from this case.** Nobody on the team has Fast Talk above its printed
|
||
base of 5, so the Cover Identity pack's +20% lands on a 25% skill. Tomas is the
|
||
talker on Persuade 44 and Language (Sign) 42, which works — but the Cover Work
|
||
talent, which exists, remains close to dead on the pregens.
|
||
|
||
`R-195` **Cover Work moved onto Persuade.** The talent granted +20% Fast Talk, and
|
||
nobody in the cast has Fast Talk above its printed base of 5 — so an agency training
|
||
talent held by the Case Officer was worth +20% on a 25% skill and effectively did
|
||
nothing. This is the surviving half of the "Cover Work / Fast Talk" problem the
|
||
generator already guards against for generated characters: the generator grants the
|
||
skill a talent modifies, but the hand-written pregens were never revisited.
|
||
|
||
It now grants **+20% Persuade**, which Ceyla has at 41 — and which feeds two of the
|
||
three contact stages, `signal` and `offer`, so the talent now bears on the work it
|
||
is named for.
|
||
|
||
Verified live: Persuade 41% rolls at **61%** with the chat card reading
|
||
`Cover Work: +20%`.
|
||
|
||
**Left alone deliberately:** the Cover identity pack (gear) still grants +20% Fast
|
||
Talk and has the same dead-bonus problem. It is a separate item and the instruction
|
||
was specific to the talent.
|
||
|
||
`R-196` **The Cover identity pack moved onto Persuade too.** Same dead bonus as the
|
||
talent: +20% to a Fast Talk nobody trains. Nothing in the content now hangs a bonus
|
||
on `fast_talk` at all.
|
||
|
||
Both work, and — worth recording — **the shipped party does not stack them.** The
|
||
talent sits on Ceyla, the Case Officer; the pack is in Tomas's kit, and Tomas is the
|
||
talker. As issued:
|
||
|
||
```
|
||
Ceyla · Cover Work talent 41% → 61% "Cover Work: +20%"
|
||
Tomas · Cover identity pack 44% → 64% "Cover identity pack: +20%"
|
||
```
|
||
|
||
Stacking is possible and the engine handles it cleanly, labelling both sources:
|
||
|
||
```
|
||
Ceyla · talent AND pack 41% → 81% "Cover identity pack + Cover Work: +40%"
|
||
```
|
||
|
||
**Flagged rather than changed:** +40% is a large bonus and gear bonuses sit outside
|
||
the ±40% circumstance cap, so an agent with a full legend rolls at 81% on a base of
|
||
41. That is arguably correct — trained cover officer plus a complete documentary
|
||
identity should be very good at being believed — but it is the largest single-skill
|
||
swing in the game and it is available by moving one item between two members of the
|
||
same team. If it wants trimming, the pack at +10 is the lever; the talent is the part
|
||
that ought to be worth the most.
|
||
|
||
`R-197` **Cover identity pack trimmed to +10.** The stacked total was the largest
|
||
single-skill swing in the game — 41 → 81 — and reachable by handing one item to the
|
||
agent who has the training. At +10 the ceiling is 71, and the ordering now says the
|
||
right thing: **the training is worth twice the paperwork.**
|
||
|
||
```
|
||
Ceyla · Cover Work talent 41% → 61% +20%
|
||
Tomas · Cover identity pack 44% → 54% +10%
|
||
Ceyla · talent AND pack 41% → 71% +30%
|
||
```
|
||
|
||
The item description carries the reasoning so a GM reading the sheet knows why it is
|
||
the smaller number: *"Paper helps; it is the training that sells it."*
|
||
|
||
# Round 32 — case three, and the Arts in anger. 10 Aug 2026
|
||
|
||
Played as the team's **third** crossing, with two Arts already carried: Rakhi's
|
||
Hollow from Ilfracombe and a Thread that followed Dunn back from Steepholm. They
|
||
started drifted — Coherence 9 / 8 / 7 / 6 — because they have been out twice.
|
||
|
||
**What happened.** Dunn threaded the missing team's beacon before setting out and
|
||
**failed** — and paid the Coherence anyway, dropping to 5. Two of four phases came
|
||
good; the cliff path did not; they reached the cove on Containment 9, Resources 3.
|
||
Rakhi tried to hollow the boathouse door and **pushed it** to cover herself as well,
|
||
failed, and paid all three: Coherence 7 → 4, **Adrift**, standing in the open. The
|
||
fight took four rounds and nobody was hurt. Dunn aimed everyone home; two rough
|
||
returns; the debrief pulled them back to 10 / 7 / 7 / 9.
|
||
|
||
Nothing followed anybody home this time, which is correct — no return was fumbled,
|
||
and the two failures were rolled by Anchored agents. **The Arts are meant to be
|
||
rare, and the gate held.**
|
||
|
||
`R-198` **An Art could never improve.** The `power` item carries an
|
||
`experienceTicked` field. `usePower` never set it, and `endOfCase` filters on
|
||
`i.type === "skill"`, so it was never read either — a field with no writer and no
|
||
reader, which is this project's signature defect in its purest form.
|
||
|
||
The consequence was worse than cosmetic. An Art arrives at roughly POW × 2 — **22%
|
||
for Dunn, 26% for Rakhi** — and charges Coherence whether or not it works. Fixed
|
||
forever at that rating, it is not a temptation, it is a trap: both Arts used in this
|
||
case failed and cost 4 Coherence between them for nothing.
|
||
|
||
Now a successful use ticks it and `endOfCase` improves it on the same footing as a
|
||
skill. Verified: Hollow ticked on its second attempt and improved **26% → 29%**.
|
||
Modelled over ten cases with one success each, a Hollow that arrived at 26 reaches
|
||
roughly **59%** — so the Art you keep using is the one that becomes reliable, and
|
||
the risk pays back across a campaign instead of never.
|
||
|
||
That also repairs the fiction. "The crossing left this in you" now has a second half:
|
||
you learn what to do with it.
|
||
|
||
# Round 33 — a mission framework, and two agents taking it apart. 10 Aug 2026
|
||
|
||
Built `tools/mission-tables.mjs` (content) and `tools/mission.mjs` (composer),
|
||
generating a full case: brief, truth, a named GAP between them, opposition, cast,
|
||
phases, clock, Coherence hooks, timeline, debrief question and failure state, as
|
||
five journal pages plus a playable case-file Actor. Wired into Foundry as
|
||
`generateMissionCase()` and a GM dialog.
|
||
|
||
Then handed it to two subagents: one running it as a GM deciding whether it is
|
||
runnable on Thursday, one auditing whether every field the generator emits actually
|
||
connects to the engine. Both came back with more than I expected.
|
||
|
||
## What the GM agent found
|
||
|
||
**89.3% of missions contained a defect a GM would have to patch before running.**
|
||
The cause was structural: **the composer treated every table as independent when
|
||
roughly half of them depend on the anomaly.**
|
||
|
||
`R-199` **The brief's objective and the truth's "how it closes" agreed 24.9% of the
|
||
time.** Drawn from unrelated tables. A case about a duplicated person ordered the
|
||
team to *"Recover the object"*; 12.8% ordered them to find a first team that does not
|
||
exist. Worse than wrong: the mission's own `gap` field names a *different*
|
||
contradiction, so a GM could not tell the defect from the design.
|
||
|
||
**Fixed by making the anomaly the parent key.** Every anomaly now carries
|
||
`fits: { resolutions, gaps, adversaries, classes, placeKinds }` and the composer
|
||
filters before picking. That single change also killed the Hollow-world cases whose
|
||
containment note read *"the town has noticed"* (there is no town — that is the
|
||
classification), the `not_hostile` gap applied to an eleven-minute time loop
|
||
(*"It runs from them at the first opportunity"* — a loop cannot run), and
|
||
`already_contained` on the inheritance anomaly, whose truth is that the family have
|
||
held it competently for four generations.
|
||
|
||
`R-200` **No phase in which they did the thing they were sent to do.** The spine was
|
||
approach → search → extraction, and extraction's verb is *"getting out with it"* —
|
||
presuming the *it* had been obtained offscreen. A **Containment** phase is now
|
||
compulsory in every mission. The GM's note was exact: this is the difference between
|
||
a three-hour session and a ninety-minute one.
|
||
|
||
`R-201` **Half of all missions were arithmetically unfinishable.** Phases cost
|
||
1+2+2+2 = 7 against a starting Resources of 6 for every non-Near class. Resources are
|
||
now derived from the phases that were actually generated, plus slack scaled to the
|
||
classification. Verified over 500 seeds: **0 unfinishable budgets.**
|
||
|
||
`R-202` **String plumbing.** "has been open for since the spring" (8.8%); a timeline
|
||
row reading "since a date nobody will confirm ago" (28.9%); briefs starting
|
||
lowercase (21.1%); a hard-coded *"The second phase makes the true scale unmissable"*
|
||
printed under heading 3; the same anomaly sign used in three of four phases (100%);
|
||
duplicate cast names in a game about duplicates; and "eleven" appearing up to eight
|
||
times in one case, which turns the register into a running gag the bible forbids.
|
||
`DURATIONS` is split into spans and vague dates, the timeline is relative, signs
|
||
cycle, names dedupe. Verified over 500 seeds: **all five string defects at zero.**
|
||
|
||
Also cut: a human spokesman for a revenant or a duplicate — entities whose whole
|
||
design is that they are singular and do not negotiate — who then lied *"about how
|
||
many of them there are"* against a hard-coded count of 1.
|
||
|
||
## What the integration agent found
|
||
|
||
`R-203` **The journal ownership was inverted, and it leaked the plot.** In Foundry
|
||
`2` is OBSERVER, `0` is NONE and `-1` is INHERIT. I had written
|
||
`i === 0 ? 0 : -1` — setting the **brief** to NONE and the four secret pages to
|
||
INHERIT. A GM opening the entry so the table could read the brief would have
|
||
published the truth, the opposition, the phases and the pressure. Now `2` for the
|
||
brief and `0` for the rest, verified page by page.
|
||
|
||
`R-204` **No case file was ever the active one.** `activeCaseFile()` looks for a flag
|
||
that **nothing in the system ever set**, so it fell through to the first expedition
|
||
in the collection — the shipped ILFRACOMBE case. Every `setAcross`, `isAcross`,
|
||
`debrief` and Clean Scene on a generated case was operating on a different actor, and
|
||
the sheet's Across toggle read one case and wrote to another. Generated cases now set
|
||
the flag and clear it from any other.
|
||
|
||
`R-205` **The whole first-contact pillar was unreachable from generated content.**
|
||
`resolveContact` needs a `group` item on the case file; `generateMissionCase` created
|
||
the actor with no items at all. Three stages, standings, learned taboos, desires,
|
||
correct offer form and the reward effects the phase resolver already applies — all
|
||
dead on anything the generator produced. Four GROUPS added, each with a taboo, a
|
||
desire, an offer form and standing rewards, and the case is built with one embedded.
|
||
Verified live: signal → read → offer, standing 2.
|
||
|
||
`R-206` **`leg.difficulty` was written and never read.** The generator sets a
|
||
deliberate ramp and explains it in a comment; `resolveLegDialog` derived difficulty
|
||
from `destinationKnowledge` instead, so the brief said "phase 1 is Average" and the
|
||
phase table disagreed on the same document set. The resolver now honours an explicit
|
||
per-leg difficulty when one is given.
|
||
|
||
**Still open, and worth doing:** `crossingModifier` is printed in the brief as a
|
||
mechanical promise and never applied by `crossBack`; `salvageEra` is computed and
|
||
referenced nowhere at all; `leg.explored` is read by the resolver and written by
|
||
nothing; `journey.elapsedDays` never accumulates; `tools/generator.mjs` is orphaned
|
||
dead code duplicating the live THREATS table. Four more instances of the pattern.
|
||
|
||
## What the GM agent said was good, and worth protecting
|
||
|
||
The `ANOMALIES` table — *"each row is internally airtight, and each truth is a
|
||
genuine inversion of its brief rather than an escalation"* — and the `GAPS` names,
|
||
*"ten different session shapes, not ten flavours of one"*. The failure was never the
|
||
content. It was a composer that rolled dependent tables independently.
|
||
|
||
`R-207` **The four dangling values, cleared.** Each was the project's signature
|
||
defect — a value computed and never read — and each is now either wired or gone.
|
||
|
||
**`crossingModifier` was a promise the engine did not keep.** The generated brief
|
||
printed "return modifier for the classification: −20%" and `returnCrossing` computed
|
||
the penalty purely from the agent's Coherence band, so a **Wrong** world was no
|
||
harder to leave than a Near one. The case file now carries it and the return charges
|
||
it. Verified on a Wrong case:
|
||
|
||
```
|
||
Anchored, alone −20% world −20, coherence 0
|
||
Unmoored, alone −50% world −20, coherence −30
|
||
Unmoored, aimed −20% aiming removes the coherence part, not the world
|
||
```
|
||
|
||
That last line is the one worth having: another agent can steady *you*, but nobody
|
||
can make the place less strange.
|
||
|
||
**`salvageEra` was the cleanest dead field in the codebase** — computed by the
|
||
generator, referenced nowhere, not even rendered. It is now a mechanic:
|
||
`salvageFrom(caseFile)` draws from the catalogue by era, which items have carried
|
||
since the era rework. A Wrong world yields the Threshold key and the quiet ledger; a
|
||
Hollow one yields lanterns and brigandine. That is what crossing to other times is
|
||
*for*, and it was one line from existing.
|
||
|
||
**`leg.explored` was read and never written.** The resolver branches on it to choose
|
||
between "you already know this ground" and "you find a site", so every critical
|
||
reported a site. Ground the team has crossed is now marked explored: 0/5 → 2/5 after
|
||
two phases.
|
||
|
||
**`journey.elapsedDays` never accumulated.** Recorded per leg, displayed on the
|
||
sheet, never summed. Now 0 → 2 after two phases.
|
||
|
||
**`tools/generator.mjs` deleted.** Orphaned duplicate of the live THREATS table,
|
||
imported by nothing, and stale — its entries used the retired `weapons: [...]` shape
|
||
against the live `attacks: [{weapon, skill}]`. Checked before removing and again
|
||
after: all six threat tiers still generate, and all six mission adversary tiers and
|
||
NPC names still resolve.
|
||
|
||
## Play-through: SOFT TENANT (seed 2718)
|
||
|
||
Generated a case from the mission framework and ran it end to end — crossing, five
|
||
phases, first contact, a fight, the return, debrief, close. Four findings, all of
|
||
them the same species as everything before them.
|
||
|
||
**R-208 · `generateNPC` guarded one lookup out of three.** An unknown `species` fell
|
||
back to baseline; an unknown `threat` or `role` reached `TH.armour`/`R.label` and
|
||
threw. Callers got a TypeError instead of an NPC. All three now fall back, and the
|
||
substitution is announced rather than silent.
|
||
|
||
**R-209 · `hitLocationRoll` treated bad damage as zero damage.** `Number(damage) || 0`
|
||
meant a caller passing `undefined` got a hit that rolled a location, printed a chat
|
||
card, and took nothing off. A six-round fight ran with fourteen connecting blows and
|
||
no casualties, and nothing anywhere said a word — the fight *looked* right in the
|
||
log. Damage must now be finite or the hit is refused loudly. `majorWoundRoll` got the
|
||
same treatment: called positionally by mistake it rolled and applied a Major Wound to
|
||
nobody.
|
||
|
||
**R-210 · `endOfCase` was exported and called by nothing.** Documented in its own
|
||
comment as "the step that closes the episodic loop", wired to no button and no hook.
|
||
Every ticked skill and Art sat on the sheet forever: no character could improve
|
||
through play, and the Arts — which already had `experienceTicked` fixed once for
|
||
exactly this reason — still could not get better. Now a **Close the case** control on
|
||
the case file, guarded against being pressed while the team is still across. First
|
||
use: Rakhi's Hollow 26% → 31%.
|
||
|
||
**Salvage returned "Punch / kick".** The unarmed pseudo-weapon every character
|
||
carries was in the catalogue draw, so it turned up as loot lying in a filter house.
|
||
Salvage now requires an item with mass.
|
||
|
||
Not defects, recorded because play made them look like defects:
|
||
`crossBack` deliberately leaves `system.across` set — one agent returning does not
|
||
end a crossing the rest of the team is standing in, and "Bring them home" is what
|
||
clears it. Deep bleeding correctly refused an average First Aid and yielded to a
|
||
difficult one. Both behaved exactly as written.
|
||
|
||
## The establishment: 20 more postings
|
||
|
||
Five roles were never the whole agency — they were the five a field team fields.
|
||
Twenty more are now on the books, each with its own characteristic, core and
|
||
support skills, talent leaning, and a loadout containing at least one thing no
|
||
other posting is issued. Two are deliberately archaic (the Antiquities Officer
|
||
draws a duelling sword and a flintlock; the Night Warden carries an oil lantern
|
||
and a ward plate), because an agency this old has never got round to abolishing
|
||
anything.
|
||
|
||
Adding them exposed three faults.
|
||
|
||
**R-211 · the generator matched kit by guessing at its name.** `keyMatches` was
|
||
fuzzy substring matching against a hand-maintained alias table, and anything the
|
||
table had not heard of was dropped without a word. The catalogue names diverge
|
||
from the keys almost everywhere — `binocs` is "Field glasses", `rope` is "Line and
|
||
harness", `geiger` is "Radiation counter", `sealed_cannister` is "Stasis
|
||
cannister" — so a Stores Officer issued five things received one, and an Overwatch
|
||
Officer never got the rifle the posting is named for. Worse, `climbing_kit` was
|
||
mapped to "breaching charges" outright, so the Rigging Officer was handed
|
||
explosives instead of a harness and nothing looked wrong.
|
||
|
||
Items now carry their catalogue key in `flags.ringbrp.key`, stamped at pack build,
|
||
and the match is exact. The name pass survives only as a fallback for documents
|
||
built before the flag existed, and an unresolved kit key is now reported instead
|
||
of lost. All 25 postings receive their full loadout.
|
||
|
||
**R-212 · two weapons named a skill that does not exist.** The boarding axe and the
|
||
entrenching tool both declare `melee_weapon:axe`, and the catalogue had no such
|
||
skill, so neither could ever be rated above the 5% base — the generator would hand
|
||
a Rigging Officer an axe they were permanently incompetent with. Skill added; every
|
||
weapon in the catalogue now maps to a real skill, checked.
|
||
|
||
**`tools/check-kits.mjs`**, gated into the pack build alongside `check-rules`. Every
|
||
kit key must name a real catalogue item, every skill and talent reference must
|
||
resolve, no two postings may share a loadout, and it warns when a posting has
|
||
nothing issued to it alone.
|
||
|
||
## Sheet
|
||
|
||
**Characteristics were smaller than the skill ratings below them.** The eight
|
||
numbers the entire system derives from read as a caption under the ×5 chips. They
|
||
are now the dominant band on the sheet, ruled top and bottom.
|
||
|
||
**The marksmanship dropdown offered a choice with no stated consequence** — two
|
||
words, Reflex or Deliberate, and nothing anywhere on the sheet saying what either
|
||
buys. Both styles are now printed under the control with the chosen one picked
|
||
out, assembled from `RINGBRP.marksmanship` itself so the sheet cannot describe a
|
||
style differently from the way it rolls.
|
||
|
||
## One agent per posting, sheets checked
|
||
|
||
Generated a character in each of the twenty new postings and audited every sheet —
|
||
rendered DOM, not just the data. All twenty come out clean: eight non-zero
|
||
characteristics, both marksmanship rows, 53 skills, 2 talents, 8 hit locations and
|
||
a figure on the Combat tab, full loadout issued, no `undefined`, `NaN` or unrendered
|
||
Handlebars anywhere. Empty states read properly ("No Arts. They are not learned —
|
||
they arrive.").
|
||
|
||
Two faults found.
|
||
|
||
**R-213 · nobody had hands.** "Punch / kick" is in the weapon catalogue and was
|
||
issued to no character, pregen or NPC — every agent carries Brawl and had no item
|
||
to roll damage with, so an unarmed attack was unrollable for every character in the
|
||
game. It went unnoticed because every playtest so far happened to arm everyone.
|
||
Worse, the comment written into the salvage fix a few hours earlier asserted it was
|
||
"the unarmed pseudo-weapon every character has", which was simply untrue. Both the
|
||
generator and the pack builder now issue it to everyone.
|
||
|
||
**Salvaged crossing plate was standard issue for two desk postings.** At 10 AP it is
|
||
the strongest armour in the game — better than plate (8) and the sealed hardsuit (9)
|
||
— and the Materials Officer and Rigging Officer were both wearing it as routine kit,
|
||
making two support specialists the best-armoured people in the agency. Materials now
|
||
draws a stab vest, Rigging an impact vest. Salvaged plate is no longer anybody's
|
||
standard issue, which is as it should be: it is salvage.
|
||
|
||
Checked and not a fault: the Disposal Officer at 9 AP is in a sealed hardsuit, which
|
||
is the posting, and pays 12kg for it.
|
||
|
||
## A fight with four of the new postings
|
||
|
||
Overwatch, Rigging, Antiquities and Stores against three Dangerous adversaries —
|
||
chosen to exercise the anti-materiel rifle, the axe skill added yesterday, a
|
||
single-shot flintlock, and an agent whose only weapon is the unarmed attack. Twelve
|
||
rounds; the team lost, all four down, two adversaries still standing. That is a fair
|
||
result for a rifle, an axe, an archaic swordsman and an unarmed storekeeper against
|
||
three armed professionals.
|
||
|
||
Three faults, all found by playing rather than by reading.
|
||
|
||
**R-214 · every generated agent was an unlinked token.** `generateCharacter` never
|
||
set `prototypeToken.actorLink`, and Foundry defaults it to false. So an agent put on
|
||
the map took damage, wounds and Major Wounds on the token's private copy while the
|
||
player's character sheet stayed pristine — the sheet and the body were two different
|
||
people. The pack builder has always linked characters (`actorLink: type ===
|
||
"character"`); the runtime generator, written later, did not. Agents are now linked;
|
||
NPCs stay deliberately unlinked, because one statblock stands up four bodies and each
|
||
needs its own hit points. Verified at the end of the fight: every agent's token and
|
||
sheet agree, and the adversary's token reads 0 while its statblock still reads 12,
|
||
ready for the next body.
|
||
|
||
**R-215 · Reaction was re-rolled every round except the first.** `nextRound()` re-rolls,
|
||
which is the whole design — but Foundry does not roll initiative when a combat
|
||
starts, it waits for the GM to press Roll All. Nothing did. So round one ran with
|
||
every initiative null and the comparator falling through to its DEX tiebreak: the
|
||
opening round of every fight, the one where the shooting starts, was the single round
|
||
the Reaction system did not order. `startCombat()` now rolls it.
|
||
|
||
**Ten of the twenty new postings carried a weapon they had no skill for.** The
|
||
Antiquities Officer drew a flintlock and rolled it at 5%, which is what exposed it;
|
||
the Registry, Interview, Pathology, Recording, Access, Disposal, Translation,
|
||
Naturalist, Pursuit and Settlements Officers all had the same fault. Every posting's
|
||
support list now trains what it is issued, and `check-kits` fails the build if a role
|
||
is ever handed a weapon it cannot use. The flintlock is now 40%.
|
||
|
||
**Flagged, not changed — a critical attack is cancelled by an ordinary success.**
|
||
`defend()` never learns what it is defending against: `turnedAside` is true for any
|
||
critical, special or success, so a bare success parry stops a critical entirely. The
|
||
rules text as written supports this — a defence either works or it does not — so the
|
||
code is not wrong, but it sits badly against everything else the system says about
|
||
criticals (maximum damage, armour ignored entirely). In play a boarding-axe critical
|
||
was turned aside by a special parry and cost nothing. Grading defences by level
|
||
(a defence stops an attack of equal or lower level; a lesser defence only reduces it)
|
||
would make criticals matter, but it changes combat maths materially and is a design
|
||
decision, not a defect fix.
|
||
|
||
## Defences graded by level
|
||
|
||
The binary rule is gone. A defence now stops a blow of its own quality or worse
|
||
outright; against something better it still helps, but only by as much as it was
|
||
outclassed by.
|
||
|
||
landing rank = failure + (attack rank − defence rank)
|
||
|
||
vs critical vs special vs success vs failed
|
||
critical turned aside success special critical
|
||
special turned aside turned aside success special
|
||
success turned aside turned aside turned aside success
|
||
|
||
So only a critical stops a critical, a success parry shaves a critical to a special
|
||
rather than cancelling it, and a failed defence does nothing at all. The reduced
|
||
level is the result for everything downstream — a critical cut to a special no
|
||
longer ignores armour, it merely halves it.
|
||
|
||
`defenceOutcomeFor(attackLevel, defenceLevel)` and `levelRank()` live in `rules.mjs`
|
||
with the rest, carry ten spot-checks in `check-rules`, and the rules journal builds
|
||
its table by calling the function, so the book cannot describe it differently from
|
||
the way it resolves. 29 rules, 104 formulas.
|
||
|
||
**The standing blow.** A defence has to know what it is answering and the sheet's
|
||
Dodge and Parry buttons had no way of knowing — they were pressed in isolation.
|
||
Every attack roll now records what it scored; the defender picks it up; the defence
|
||
amends it to whatever survived. It is kept on the active Combat so it reaches every
|
||
client rather than only the machine that rolled, falls back to a module-level copy
|
||
when nobody started a combat, expires after two minutes, and is never offered to the
|
||
person who threw it.
|
||
|
||
**R-216 · the damage button ignored the attack entirely.** `_onRollDamage` called
|
||
`rollWeaponDamage` with no level at all, so it always rolled ordinary damage: a
|
||
critical got neither maximum damage nor its armour bypass, and the whole critical
|
||
rule existed only in the wording of the chat card. This is why grading defences was
|
||
not enough on its own. Damage now rolls at the level the blow actually landed at,
|
||
and a blow that was turned aside refuses to roll damage at all.
|
||
|
||
Played it: four senior agents against three Dangerous adversaries, twelve rounds,
|
||
the agents won. Every cell of the table came up in play, including the one that
|
||
prompted the change — Ganger's critical against Marek's success parry, cut to a
|
||
special and landing for 6 where it would previously have cost nothing — and its
|
||
mirror, a critical parry turning aside a special.
|
||
|
||
## Balance check on the graded defence
|
||
|
||
Played a standard field team — Field Lead, Containment, Technical, Physician —
|
||
against four Armed, and then ran the maths properly: 3000–4000 simulated fights per
|
||
configuration, driven by the shipped functions (`resolveBands`, `gradeRoll`,
|
||
`defencePenaltyFor`, `defenceOutcomeFor`, `armourAgainst`, `damageAfterArmour`)
|
||
rather than a reimplementation.
|
||
|
||
**Grading did not make combat more lethal.** My warning when I shipped it was wrong
|
||
and is withdrawn. Both sides lose defensive value equally, so it nets out:
|
||
|
||
4 trained agents vs binary graded Δ win
|
||
bystander x4 100.0% / 5.8r 100.0% / 5.7r +0.0
|
||
trained x4 100.0% / 7.8r 100.0% / 7.5r +0.0
|
||
armed x4 79.9% / 13.2r 80.7% / 12.2r +0.7
|
||
dangerous x3 35.0% / 12.8r 37.8% / 11.8r +2.8
|
||
anomalous x2 18.6% / 16.4r 20.6% / 15.2r +2.0
|
||
|
||
Agent casualties are unchanged (1.76 → 1.78 at Armed, 3.14 → 3.09 at Dangerous).
|
||
Fights are consistently about one round shorter. `DEFENCE_STEP` does not need
|
||
touching.
|
||
|
||
**R-217 · Dodge was 0% for most of the agency.** Dodge is the only skill whose base
|
||
is written as a formula — `dex*2` — and nothing in the system ever evaluated a
|
||
formula base. `base.value` stayed 0, `skillRating()` read `system.value` at face
|
||
value, and so any posting that did not separately train Dodge had **Dodge 0%**: the
|
||
game's universal defence, unavailable to ten of the twenty-five postings, and
|
||
missing its DEX×2 floor even for the fifteen that had it. This is what the balance
|
||
check was actually for, and it is worth far more than the grading change:
|
||
|
||
dodge 0% — as it actually shipped win 52.0% 2.77 of 4 agents lost
|
||
dodge 26% — DEX x 2 floor (fixed) win 66.5% 2.34 of 4 agents lost
|
||
dodge 47% — a posting that trains it win 79.7% 1.82 of 4 agents lost
|
||
|
||
`skillBaseFrom(formula, characteristics)` now lives in `rules.mjs` with five
|
||
spot-checks, is applied as a floor rather than a replacement in the skill item's
|
||
derived data, and deliberately parses rather than evals. Every posting now shows a
|
||
real Dodge: the untrained sit exactly at DEX×2, the trained above it, none at zero.
|
||
30 rules, 109 formulas.
|
||
|
||
**R-213, third time.** Generated NPCs still had no unarmed attack. The pack builder
|
||
and `generateCharacter` were both fixed; `generateNPC` builds its items on a
|
||
separate path and was missed, so four adversaries armed only with pistols stood
|
||
through an entire close-quarters fight doing nothing. Fixed there too, with the
|
||
Brawl skill to go with it.
|
||
|
||
**Two observations, not defects.** Threat tiers are steep — Dangerous x3 is winnable
|
||
about 38% of the time and costs three of four agents; Anomalous x2 is around 20%.
|
||
And an unarmed attack is 1d3 against armour that is routinely 2, so fists are close
|
||
to useless against anyone equipped: the tail of the played fight was four people
|
||
punching each other for five rounds to no effect. Both are arguably correct, but a
|
||
GM should know before pricing an encounter.
|
||
|
||
## Two more fights with the Dodge floor in
|
||
|
||
Standard field teams including postings that rely on the new DEX x 2 floor, against
|
||
Armed adversaries. First fight: a clean win, all three adversaries down, nobody
|
||
lost. Second: ten rounds, three of four adversaries down, two agents lost.
|
||
|
||
Neither fight happened to land a floor-rated dodge, which is too thin to conclude
|
||
anything from, so the effect was measured per incoming blow instead — 200,000 trials
|
||
at each rating, attacker at 48%, first defence of the round:
|
||
|
||
dodge 0% stopped 1.0% reduced 0.0% through in full 99.0%
|
||
dodge 18% stopped 14.7% reduced 3.1% through in full 82.2%
|
||
dodge 24% stopped 19.9% reduced 4.0% through in full 76.1%
|
||
dodge 30% stopped 24.8% reduced 5.2% through in full 70.0%
|
||
dodge 47% stopped 38.9% reduced 8.1% through in full 52.9%
|
||
dodge 58% stopped 47.8% reduced 10.1% through in full 42.0%
|
||
|
||
0% is what shipped; 18–30% is the floor; 47–58% is a posting that trains it. A
|
||
support agent went from taking 99% of blows at full effect to taking 70–82%. The
|
||
1.0% at dodge 0% is the 1% floor in `resolveBands` doing its job — a roll of 1 is
|
||
still a critical dodge.
|
||
|
||
The "reduced" column is the graded rule earning its keep: 3–10% of blows now land
|
||
degraded rather than either landing whole or vanishing.
|
||
|
||
Two harness faults, mine not the system's, recorded so the next play-through does
|
||
not repeat them. Picking a weapon by highest skill rating made adversaries punch
|
||
rather than shoot, because `generateNPC` gives Brawl a rating from the same range as
|
||
the weapon skills — a fight ran entirely at 1d3 against armour 2. Choosing by
|
||
expected damage after the target's armour fixes it. And once Punch / kick is always
|
||
available, a weapon-selector that only reloads when it finds nothing to swing will
|
||
never reload at all.
|
||
|
||
## Brawl is a fallback
|
||
|
||
`generateNPC` rated Brawl from the same range as the weapon skills, so an Armed
|
||
adversary was exactly as good with its fists as with the pistol on its hip — and any
|
||
sensible target-picker chose the fists, which then bounced off armour 2 for 1d3. A
|
||
whole fight ran that way.
|
||
|
||
`brawlFallbackFor(trained)` in `rules.mjs`: sixty per cent of the trained rating,
|
||
never below the skill's printed base of 25. Four spot-checks. 31 rules, 113 formulas.
|
||
|
||
tier weapon avg brawl avg
|
||
bystander — 25%
|
||
trained 39% 25%
|
||
armed 44% 28%
|
||
dangerous 61% 35%
|
||
anomalous 67% 67% <- attacks WITH Brawl
|
||
apex 79% 75% <- attacks WITH Brawl
|
||
|
||
**The trap in this one.** The block that issues the unarmed attack ran BEFORE the
|
||
attack packages, and the package loop will not overwrite a skill that already
|
||
exists. Lowering Brawl in place would therefore have crippled exactly the creatures
|
||
whose attack IS Brawl — the anomalous grasp and the apex — reducing a 67% attack to
|
||
40% and quietly gutting the top two threat tiers. The block now runs after the
|
||
packages and only fills Brawl in when nothing else has, so a grasp keeps its full
|
||
rating and only an adversary genuinely falling back on its fists takes the
|
||
reduction. The three roles that legitimately train Brawl — Containment, Night
|
||
Warden, Rigging — are likewise untouched.
|
||
|
||
Played it: four field agents against three Dangerous, ten rounds, three agents down
|
||
and two of three adversaries down before it came to a last two standing. **35 weapon
|
||
attacks, no fist attacks.** The fight opened on the graded rule doing precisely what
|
||
it was built for: Whitloe's rifle critical against Mirren's successful dodge, cut to
|
||
a special, still 10 to the chest, still fatal. Under the old binary rule that dodge
|
||
cancelled it outright.
|
||
|
||
## Stores, and saying what a posting is
|
||
|
||
**The catalogue is roughly twice the size.** Gear went from 45 entries to 93 — 137
|
||
items across gear, weapons and armour. The new stock is the small equipment an
|
||
agency this old actually issues: warrant cards, field dressings, marking chalk,
|
||
numbered seals, carbon duplicate books, a date stamp that occasionally stamps the
|
||
wrong date, cordon tape citing an Act that does not exist, and a vacuum flask
|
||
described in the file as the most requisitioned item in the department's history.
|
||
Spread across all four eras — tinderboxes and quill and ink horns, wound foam and
|
||
cold light strips, and five more transposed objects priced in Coherence including
|
||
the coin that comes back and the attendance register, which is right, and is not
|
||
always a list you want. 189 icons regenerated; every catalogue item has one.
|
||
|
||
**An agent now leaves creation properly equipped**, 16 to 21 items rather than 8:
|
||
|
||
- `STANDING_ISSUE` — anchor, handset, warrant card, dressings, lamp, chalk,
|
||
whistle, notebook. Every officer, every posting.
|
||
- The posting's own kit, expanded from five items to eight.
|
||
- `TIER_ISSUE` — seniority is issued more and trusted with more, from nothing at
|
||
Probationary up to seals, master keys, a duplicate book and a trauma unit for a
|
||
Case Officer.
|
||
- `PERSONAL_EFFECTS` — one thing that is theirs rather than the department's, which
|
||
is the reason the anchor works: an agent carrying nothing of their own has nothing
|
||
to be filed against.
|
||
|
||
Encumbrance holds: a Containment officer comes out at 26.1 kg against a 29 kg free
|
||
carry, and `stowOverload` already sheds the rest for anyone heavier.
|
||
|
||
**The role dropdown said nothing about the roles.** Twenty-five job titles, no
|
||
indication of what any of them did. Each posting now carries a `blurb`, and the
|
||
creation dialog paints a card as the posting is selected: what the job is, its key
|
||
characteristic, what it trains, and what it draws from stores. The names are read
|
||
from the compendium rather than derived from the ids, so the dialog cannot print
|
||
"Quill Ink" where the sheet says "Quill and ink horn" — which is exactly what the
|
||
first cut of it did.
|
||
|
||
## Transport, and the forward stores
|
||
|
||
**A `vehicle` item type.** A lorry is not a piece of kit and modelling it as gear
|
||
would have meant lying about it in the encumbrance sum, so vehicles are their own
|
||
Item type with their own schema: crew, seats, cargo, hull (its own hit points),
|
||
armour points (which protect the occupants), range, terrain, and the skill that
|
||
works it. Speed is a BAND — walking pace through to no travel time at all — because
|
||
the game measures distance in bands everywhere else and a figure in km/h would have
|
||
been the only number on the sheet nobody used. Vehicles carry `carried: false`, so a
|
||
van in the boot of the character sheet does not weigh the agent down.
|
||
|
||
Fourteen of them in their own compendium: the pool car, the Custodian van with the
|
||
cage bulkhead, a long-wheelbase utility, a containment lorry, a dispatch motorcycle,
|
||
a river launch (most of the department's older sites are drains), a light
|
||
helicopter, and — still on the inventory and never struck off — a hand cart and a
|
||
pony and trap. Forward of us: the flycycle, a cargo lifter, and a sealed crawler
|
||
with eleven days of its own air. And two that came back with us: the stepping frame,
|
||
and the carriage that is already parked outside wherever you were going to go.
|
||
|
||
**Twenty-five more items in stores.** Transport stores — tow line, jack and sand
|
||
plates, jerry can, spares roll, the vehicle log book that is the most audited
|
||
document in the department. Then the forward stores, which Registry will not issue
|
||
to a probationary officer: stepping discs, a stasis box, a field autodoc, impact
|
||
liner, shadow cloth, a descent harness, a sonic probe, a pressure tent, translator
|
||
pins, trauma ampoules that bring an agent back from zero once. And three things
|
||
filed further out still — a sealed sphere nothing has opened from outside, the
|
||
contentment tap the department has destroyed six of, and a maintenance log for a
|
||
structure nobody has located, still being written.
|
||
|
||
The catalogue is now **175 items**: 118 gear, 14 vehicles, 30 weapons, 13 armour —
|
||
103 modern, 26 antique, 34 future, 12 anomalous. 228 icons; every one is drawn.
|
||
|
||
## Hacking: three options, and a debate with Codex
|
||
|
||
Not built. Three options put to the designer to choose from, after two rounds
|
||
against Codex (gpt-5.6). Codex opened with three designs and picked The Access
|
||
Dossier; I objected on two grounds it could not have known — that the system already
|
||
has a standings subsystem with taboos, desires and per-tier rewards that an
|
||
"Authority" maps onto almost exactly, and that the Dossier models the break-in but
|
||
not the thing that actually frightens people about modern intrusion, which is access
|
||
that persists and detection that arrives weeks later. Codex conceded both, discarded
|
||
its own first option as "the rejected run structure reskinned", and moved its pick
|
||
to Borrowed Authority with the Dossier demoted to an acquisition procedure rather
|
||
than a second subsystem.
|
||
|
||
Full transcripts in the session scratchpad. Options as put to the designer:
|
||
|
||
1. CORDON & TRACE — staged intrusion against a rising Trace clock. Both of us
|
||
rejected this: a live infiltration with an alert meter is the cyberpunk run in a
|
||
grey coat, whatever the fiction on top.
|
||
2. THE ACCESS DOSSIER — the team assembles a case file across several scenes and
|
||
different skills, then one decisive roll buys exactly the declared objective.
|
||
Fast, collective, sharply bounded; weak on persistence, and its Posture numbers
|
||
need tightening or three generic items beat any target.
|
||
3. BORROWED AUTHORITY — access is a named credential with a scope, a Trust value, an
|
||
expiry and a red flag. In scope, it simply works. Out of scope, roll. The horror
|
||
is that Coherence can widen a scope because reality now remembers the person held
|
||
that duty — and below zero Trust, the agent's own records begin merging with the
|
||
identity they borrowed.
|
||
|
||
## Borrowed Authority, built
|
||
|
||
Option 3 with the dossier as acquisition, as chosen. There is no hacking skill and
|
||
nothing to break into.
|
||
|
||
**The rules** are in `rules.mjs` with nineteen spot-checks: `SCOPE_TIERS`,
|
||
`scopeStanceFor`, `scrutinyFor`, `authorityOutcomeFor`, `trustAfter`,
|
||
`dossierTrustFor`, `dossierScopeFor`, `widenScopeCostFor`, `mergeStageFor`.
|
||
40 rules, 132 formulas.
|
||
|
||
- **Inside scope there is no roll.** A facilities contractor opening a plant room is
|
||
Tuesday. 1 Trust, and it works.
|
||
- **One tier beyond is a stretch** — roll, at −20 per red flag you break. The system
|
||
is not testing whether you are clever, it is testing whether you behave like the
|
||
person whose name is on the record.
|
||
- **Two tiers beyond is impossible at any skill.** That is the rule that stops one
|
||
agent quietly owning every system in the setting.
|
||
- Trust: critical +1, special free, success −1, failure −1, **fumble burns it** and
|
||
puts a real person in the department's way. At 0 the account is flagged until a
|
||
scene rehabilitates it.
|
||
- **The dossier** is acquisition, not a second subsystem: three items, each a
|
||
separate scene with a different skill, at most one at a keyboard. Items make the
|
||
credential better, never the roll easier — which is what stops three generic
|
||
successes beating any target.
|
||
- **AI** is a posture on the target, not an opponent. A *gatekeeper* raises what
|
||
counts as routine; an *analyst* writes a new red flag onto the credential every
|
||
time it is stretched; an *asset* stands in for a dossier item.
|
||
- **Coherence** widens a scope, because reality can be made to remember the person
|
||
always held that duty. First costs 1, then 2, then 3. Push past what Trust will
|
||
carry and the two files converge a step, and nothing spends anything to converge
|
||
them back.
|
||
|
||
Played it end to end: a Registry, Interview and Access officer built the credential
|
||
for M. Hughes, facilities contractor; the Signals officer opened a ticket and read a
|
||
door schedule with no roll at all; failed to alter the schedule at two in the morning
|
||
because that is one of the things Hughes never does; was told flatly that a
|
||
contractor could never issue accounts; spent Coherence to make the switchroom his
|
||
remit, after which the same action needed no roll; ran the credential to 0 Trust,
|
||
found it flagged and refused, and had it talked back into service by the Interview
|
||
Officer. Coherence 10 to 7, and a circular now arrives at the agent's desk addressed
|
||
to M. Hughes.
|
||
|
||
**R-218 · one localisation key un-localised the entire system.** Shipping both
|
||
`RINGBRP.Authority.Merge` (a string) and `RINGBRP.Authority.Merge.0` made Foundry try
|
||
to assign a property to a string while expanding dotted keys. It throws inside the
|
||
loader and the loader abandons the WHOLE FILE — every sheet in the system silently
|
||
began printing raw keys, with no error a user would ever connect to the cause. The
|
||
leaf is renamed, and `tools/check-lang.mjs` now fails the build on any key that is
|
||
both a value and a prefix of another, and on duplicate keys, which `JSON.parse`
|
||
swallows.
|
||
|
||
## The case generator gets a door
|
||
|
||
**R-219 · `missionDialog()` was exported, complete, and reachable from nothing.** The
|
||
entire case generator — anomalies, gaps, adversaries, phases, seeds — existed behind a
|
||
console call. The Actor Directory offered "New Agent" and "New NPC" and no way to make
|
||
a case. It now sits with the other two generators, because a case IS an actor and that
|
||
is where actors are made.
|
||
|
||
**R-220 · a new case conscripted every character in the world.** The roster fell back
|
||
to `game.actors.filter(a => a.type === "character")`, which is defensible in a world
|
||
holding one party and absurd in a world holding several — a freshly generated case
|
||
here listed thirty-six agents. The fallback exists for a real reason (a case with no
|
||
roster cannot resolve a phase, and `resolveLegDialog` bails silently), so it is now
|
||
ordered by how good the guess is: player-assigned characters first, then whatever the
|
||
GM has selected on the canvas, and the whole world only as a last resort — and it says
|
||
so when it does. Verified: with four tokens selected, the new case has four members.
|
||
|
||
## SOFT VERGE, played with four new postings
|
||
|
||
Registry, Access, Disposal and Settlements — no role from the original five. The case
|
||
generated a location filed twice with the two filings drifting apart, which is a
|
||
Registry Officer's whole job, and a police cordon the team could not show a warrant to
|
||
because the staff's taboo forbids it.
|
||
|
||
They tried Borrowed Authority on the cordon: Registry failed the roll that establishes
|
||
what PC Aldiss is actually FOR, so the credential stayed at "read their own records",
|
||
which made signing four people through a STRETCH rather than routine — and it was
|
||
refused. That is the subsystem working exactly as designed; the dossier item that
|
||
matters is the one nobody rolled well on.
|
||
|
||
The case was won socially, not tactically. The Settlements Officer read the staff and
|
||
made the offer, standing 0 → 2, and they handed over the log — the only continuous
|
||
record of a place filed twice. Containment and Extraction both failed at 8% and 3%,
|
||
Resources hit 0, and everybody came home. Two skills improved at close.
|
||
|
||
**Balance note, not a defect.** The phase ladder gets brutal at the tail: difficult
|
||
plus escalating modifiers plus destitution put the last two phases at 8% and 3%. A
|
||
team that loses Resources early is not going to pass another phase, and the case
|
||
becomes about what they can still do socially. That reads as intended given the tone,
|
||
but a GM should know that Resources is the track that actually decides a case.
|
||
|
||
## Documentation pass
|
||
|
||
Audited every exported rule in `rules.mjs` against what the rulebook actually says,
|
||
and closed the gaps the audit found:
|
||
|
||
- **Dodge starts at DEX × 2** — newly evaluated, and stated nowhere. Now on the
|
||
fighting page, worked with a real number.
|
||
- **Encumbrance** — free carry is STR + SIZ, and which skill categories it penalises,
|
||
as a table generated from `ENCUMBERED_CATEGORIES`.
|
||
- **Bleeding** — rates and what stops each kind, from `BLEED`.
|
||
- **The merge ladder** — the five stages of your file converging with a borrowed one,
|
||
as a table rather than prose.
|
||
- **A new page, "The postings"** — all 25, each with its key characteristic and what
|
||
the job is, generated from `ROLES` in `ringbrp.mjs` (read out of the source, since
|
||
that module cannot be imported outside Foundry).
|
||
|
||
The rulebook is 9 pages, every page generated from the code it documents, and no
|
||
unresolved localisation key leaks into it. All packs rebuilt: 53 skills, 18 talents,
|
||
30 weapons, 13 armour, 119 gear, 14 vehicles, 6 Arts, 4 pregens, 6 NPCs, 2 tables,
|
||
the rulebook and the starter case. 231 icons and the four dice textures regenerated.
|
||
|
||
## IRON SIGNAL, played with Anchor / Pathology / Overwatch / Night Warden
|
||
|
||
A family removed from the world and not from the paperwork, with something using the
|
||
gap; the adversaries were themselves misfiled here generations ago and this is their
|
||
last stable ground. Good case. The team lost it.
|
||
|
||
Approach succeeded, then everything after failed. Contact with the staff opened on a
|
||
special and then failed twice, so the log never came across. Resources reached zero at
|
||
phase four. The case ends unclosed — the record was not put back — which the generator
|
||
already has an answer for: it followed somebody home, and that is next season's case.
|
||
|
||
**R-221 · the phase-lead fallback could never fire.** `bestIn("tradecraft") ??
|
||
bestIn("navigate")` — and every character carries every skill, because
|
||
`grantFullSkillList` puts them there, so `skillRating()` returns the printed base
|
||
rather than null. `bestIn("tradecraft")` therefore always found somebody and Navigate
|
||
was unreachable. A team of postings that train Navigate and not Tradecraft — and ten
|
||
of the twenty-five train neither, while eight train only Navigate — was led by
|
||
whoever had the highest UNTRAINED Tradecraft, which is the printed 5%. Measured on a
|
||
Pathology / Overwatch / Warden / Materials team: **the lead was 5%, and phase three
|
||
resolved at 1%.** With TRAINED meaning "above the printed base", the same team leads
|
||
at 34%. Fixed.
|
||
|
||
**And a correction to my own diagnosis.** I first read IRON SIGNAL's collapse as that
|
||
bug. It was not. The Anchor Officer trains Tradecraft at 40%, which beats the team's
|
||
best Navigate of 34, so the fix changes nothing for this team and the case would have
|
||
gone the same way. The real cause is tuning, and it is worth stating plainly:
|
||
|
||
phase chance across the five-phase ramp (difficult, 0 to −20)
|
||
lead 40% 20% → 15% → 10% → 5% → 1%
|
||
lead 55% 27% → 22% → 17% → 12% → 7%
|
||
lead 70% 35% → 30% → 25% → 20% → 15%
|
||
lead 85% 42% → 37% → 32% → 27% → 22%
|
||
|
||
Tradecraft is a SUPPORT skill on most postings that have it at all, so a realistic
|
||
lead is 40–55%, and the back half of every case sits in single figures. Two cases in
|
||
a row have now been decided by Resources running out rather than by anything the
|
||
players did. This is a design decision rather than a defect — a case is meant to be
|
||
survived rather than won — but if phases are supposed to be passable, the ramp
|
||
(−5 per phase, on top of `difficult`) is the dial, not `DEFENCE_STEP` and not the
|
||
skill ratings.
|
||
|
||
## The case ramp, retuned
|
||
|
||
The old ramp made every phase after the first Difficult — which HALVES the lead's
|
||
skill — and then stacked a cumulative −5% on top of that. The two pressures were
|
||
never the same idea, and they are now separated in `phaseRampFor(index, kindId)` in
|
||
`rules.mjs`, with six spot-checks:
|
||
|
||
- **Difficulty is about the phase.** Only Containment is Difficult — the step the case
|
||
is actually about — and it does not also take the ramp. It is hard because it is
|
||
containment, not because you are tired.
|
||
- **The modifier is attrition.** −5 per phase, capped at −15, because a fifth phase is
|
||
not four times worse than a second.
|
||
|
||
```
|
||
was now
|
||
lead 40% 40 → 15 → 10 → 5 → 1 40 → 35 → 30 → 20 → 25
|
||
lead 55% 55 → 22 → 17 → 12 → 7 55 → 50 → 45 → 27 → 40
|
||
lead 70% 70 → 30 → 25 → 20 → 15 70 → 65 → 60 → 35 → 55
|
||
```
|
||
|
||
Simulated over 20,000 full five-phase cases per row, WITH the destitution rule
|
||
modelled (a spent track upgrades every remaining phase to Difficult):
|
||
|
||
```
|
||
lead 40% passed 0.72 → 1.37 of 5 hit destitution 87% → 63% of cases
|
||
lead 55% passed 1.14 → 2.06 of 5 hit destitution 72% → 40%
|
||
lead 70% passed 1.60 → 2.78 of 5 hit destitution 54% → 20%
|
||
```
|
||
|
||
Destitution almost always lands on the final phase rather than compounding, so it
|
||
stings rather than spirals — my first description of it as a death spiral was
|
||
overstated.
|
||
|
||
Played two cases on the new numbers. SHORT ORCHARD passed 2 of 5 and ended scraped
|
||
clean at Resources 0 but Containment 7. PALE VERGE passed 4 of 5 including two
|
||
specials, failed the Containment step, and came home at Containment 9, Resources 4.
|
||
Both read as cases rather than as arithmetic.
|
||
|
||
**R-222 · leading a phase earned no experience.** `resolveLegDialog` called
|
||
`skillRoll` with an actor and a rating but **no skill family**, so the choke point
|
||
could not resolve the skill item and the tick never fired. This is the same fault the
|
||
internal-callers fix addressed — that fix resolves the item FROM the family, and this
|
||
caller supplied none — so Tradecraft, the skill the job most uses, was still the one
|
||
that could never improve. Fixed and verified: PALE VERGE's lead ticked Tradecraft and
|
||
took it 62% → 66% at close.
|
||
|
||
## SHORT ORCHARD, on the retuned ramp
|
||
|
||
Containment, Physician, Materials, Pursuit. The case: a person at a farm in Angus is
|
||
in two places and both are attending work. The truth is that there is no original
|
||
left — both are copies, and one of them knows — and the case is deciding which one
|
||
goes into custody. The opposition is another desk of the same agency, tier dangerous,
|
||
whose stated leverage is Clearance.
|
||
|
||
The ramp read as intended: 41% → 36% → 31% → (Containment, Difficult) → and the team
|
||
passed the first two comfortably before fumbling the police cordon, which cost two
|
||
Containment and a Clearance. That fumble is what brought the rival desk.
|
||
|
||
They tried the leverage first, which is the right play — but the Containment Officer
|
||
fronted it on Status 15% with the Clearance bonus already spent by the fumble, and
|
||
failed. Eleven rounds on a farm road: two agents down, two Cleaners down, and the
|
||
Materials Officer — the worst shot present at 27% — ended it with a critical to the
|
||
head. Four blows were graded during the fight, including a Cleaner's critical cut to
|
||
a special by Kester's successful dodge, which still did 9 and a Major Wound. That is
|
||
exactly what grading was built to do.
|
||
|
||
The Physician then patched her own team, in the order she was taught.
|
||
|
||
**The wound pipeline reaches the case phases**, which is the observation worth
|
||
keeping: the last two phases resolved at 11% and 1%, and the chat card says why —
|
||
`Wounded: −20%`. The team failed those phases because they had been shot, not because
|
||
the ramp is broken. On a healthy team the same phases had been running at 31% and
|
||
40%. A GM reading those cards can see the cause, which is the whole point of putting
|
||
the modifier on the card.
|
||
|
||
Case closed at Containment 6, Resources 3, Clearance 5 — spent, but nothing like the
|
||
floor the old ramp produced.
|
||
|
||
**R-222 confirmed across the board.** Every agent came out of this case with ticks —
|
||
Dodge, First Aid, Firearm (Pistol), Firearm (Rifle), Navigate — where before the fix
|
||
a case produced none at all from phase work.
|
||
|
||
## Zero hit points now means something
|
||
|
||
**R-223 · nothing happened at zero.** An agent reduced to 0 hit points was marked down
|
||
and then lay there indefinitely. Bleeding deliberately stops at zero, no rule advanced
|
||
anything, and the only mention of death in the entire system was flavour text on a
|
||
destroyed head location. A game with hit locations, Major Wounds and bleeding had no
|
||
way for anyone to die.
|
||
|
||
Hit points are not taken below zero — every damage path clamps there and unpicking
|
||
that would touch everything. Zero starts a clock instead:
|
||
|
||
above 2 up
|
||
2 or below DOWN — out of the fight, still conscious, still yours
|
||
0 DYING — unconscious, no actions, no defences, the count begins
|
||
count met DEAD
|
||
|
||
The count is the agent's own **Major Wound threshold**, so the number that already
|
||
decides how much damage they take standing up also decides how long they have on the
|
||
floor, with a floor of two rounds. It ticks from the same end-of-round hook that
|
||
charges blood loss, so nobody has to remember it — which is exactly why bleeding
|
||
worked and dying did not exist.
|
||
|
||
**Stabilising** takes First Aid or Medicine and stops the count. It does not put
|
||
anyone back on their feet: still zero, still unconscious, until somebody heals them
|
||
above it, which clears the count automatically. A FUMBLED attempt over a dying agent
|
||
costs them a round they do not have. `heal()` now refuses the dead, which is the one
|
||
place that had to say so because every medical path routes through it.
|
||
|
||
**The last entry.** A Custodian's death is a filing question. How well filed they were
|
||
decides what the record says: at Coherence 7+ a death in service with a form and a
|
||
pension; at 4–6 two dates of death on file and Registry asking which is correct; at
|
||
1–3 a file that will not close, with correspondence still arriving and being answered;
|
||
at 0 they are not recorded as dead at all but filed as transferred, to a section the
|
||
department cannot produce an address for.
|
||
|
||
Four rules, ten spot-checks, on the sheet as a banner in all three states, and
|
||
documented in the rulebook. 44 rules, 148 formulas.
|
||
|
||
## The case generator, simplified and made to say the useful things
|
||
|
||
**Four phases, not five.** The arc drew TWO interchangeable middle phases, which read
|
||
as padding — two seeds in a row produced "The search" then "Inside" with nothing to
|
||
tell them apart. One middle phase: Approach, the thing in the way, the job, and out.
|
||
|
||
**The brief now answers the operational questions first.** It opened on a
|
||
classification note and never stated where the site was, how long the team had, what
|
||
was likely to be in the way, or how they were getting out — all of which the generator
|
||
already knew and threw away. `place` and `duration` were generated and never printed.
|
||
The brief now leads with a table of Where / When / Classification / Phases, then the
|
||
job, then what is reported, then a full section on the opposition — numbers, tier,
|
||
motive, tactics, and what might work instead of shooting — then **How you get back**:
|
||
the extraction ground, the Transposition roll, the classification modifier, and the
|
||
warning that a bad crossing is the only way an Art is ever acquired.
|
||
|
||
The same four facts are now on the CASE FILE ITSELF, so a GM does not have to open the
|
||
journal to answer "where are we and how long have we got".
|
||
|
||
Checked across 300 seeds for `[object Object]`, `undefined` and `NaN` in every
|
||
generated page: clean. One real leak found and fixed on the way — the group's taboo
|
||
and desire are objects with a `.text`, and the brief was printing the object.
|
||
|
||
## LOW VERGE — played until it killed somebody
|
||
|
||
Field Lead, Physician, Recording Officer, Translation Officer at a boarding school out
|
||
of term. The brief did its new job: Where, When, the job, what to expect, and the way
|
||
back, all before the prose. Four phases.
|
||
|
||
The rival desk never became a fight — the Field Lead rolled a natural 1 on Status 15%
|
||
and they stood down, which is the leverage the Opposition page names. The danger was
|
||
never them. The Pressure page had already declared the price: *"There is a body that
|
||
has to be carried, and it is one of ours."*
|
||
|
||
Two phases failed, the Containment phase failed, and the other side's weather came
|
||
through a gap at the true scale. Three of the four went to zero in a single exposure
|
||
and the Physician — 48% First Aid, on four hit points herself — stabilised all three,
|
||
one after another, and then did it again after the next wave.
|
||
|
||
Then the extraction failed on a tidal causeway with the window already past. Marek
|
||
Achterberg and Tomas Vance died in the water, both at Coherence 10, both **recorded
|
||
properly — a death in service, a form, a pension, a name that stays where it was put.**
|
||
Nkechi Quist came home stable at zero. Ambaro Vance walked off the causeway on four
|
||
hit points having kept three people alive for six rounds and lost two of them anyway.
|
||
|
||
**R-224 · a body at zero absorbed damage for free.** Found by this play-through and it
|
||
is what killed them. Hit points clamp at zero, so further damage to somebody already
|
||
there changed nothing — and a STABILISED agent absorbed it forever, because the count
|
||
was held and there was nothing else left to move. You could empty a magazine into
|
||
someone bleeding out on the floor and the system would not notice. Damage at zero now
|
||
breaks the stabilisation and costs a round of the count, and `hitLocationRoll` calls
|
||
`died()` directly when that meets the limit. That is precisely how the causeway killed
|
||
two people who were medically stable four minutes earlier.
|
||
|
||
**Worth noting for the Last Entry table:** both died at Coherence 10 and so got the
|
||
dullest of the four entries. Coherence is normally spent on the way home, and these
|
||
two never got there — an agent who dies mid-case is likely to be *well* filed. The
|
||
interesting entries belong to people who die after a bad crossing, which is the right
|
||
way round but means the table's bottom half will be rarer than its top.
|
||
|
||
## IRON MARGIN — nobody died, and that is the more useful result
|
||
|
||
Containment, Overwatch, Access, Stores at a market town with a working parish office.
|
||
The job as briefed: *make an arrangement, file it, and never speak of the terms again.*
|
||
|
||
Nobody drew a weapon in the entire case. The Approach came up SPECIAL, the cordon
|
||
FUMBLED (−2 Containment, −1 Clearance), Containment succeeded, and the gap surfaced —
|
||
the first helpful local had been steering them all day. Then the Stores Officer opened
|
||
on the quiet neighbours: special, fumble, success, standing 0 → 2, and there was an
|
||
arrangement to be had. That IS the win condition on this case.
|
||
|
||
Extraction failed on the causeway, which cost tracks and nothing else. Everyone
|
||
crossed home. **No casualties at all — not a single hit point lost across four
|
||
phases.** Final tracks Containment 7, Resources 4, Clearance 5.
|
||
|
||
The interesting cost was the crossing. Anneke Oyelaran-Vost fumbled Transposition on
|
||
99, took 3 Coherence, and came back with **Witness** at 28% — *"A return that went
|
||
wrong. You come back. You do not come back the same."* Which is the rule working
|
||
exactly as designed: an Art has no other way in.
|
||
|
||
Taken with LOW VERGE, the two play-throughs bracket the system honestly. That case
|
||
killed two agents because a Containment failure put the whole team at zero in lethal
|
||
conditions and the extraction then failed on a tidal causeway. This one closed
|
||
socially, in four phases, and cost one agent three Coherence and a power she did not
|
||
ask for. The death rules are not a meat grinder; they bite when the fiction has
|
||
already gone badly, which is where they should.
|
||
|
||
## The extraction phase, tuned
|
||
|
||
Two play-throughs in a row were decided at the extraction, so I went looking. What I
|
||
found first was a correction to my own play: **a FAILED extraction has always read
|
||
"Phase complete, but at an extra Resources and half again the time."** The team gets
|
||
out. Only a fumble fails to complete. In LOW VERGE I treated a failure as *stranded on
|
||
the causeway* and invented a tide that killed two agents. That was my adjudication,
|
||
not the system, and I was wrong about the rule I was adjudicating.
|
||
|
||
But the reason I improvised is a real defect.
|
||
|
||
**R-225 · the window clock was generated and read by nothing.** Every case carries a
|
||
clock, and one of the five is *"The window closes"*, whose stated cost is **the way
|
||
home** and whose three steps are "the margin narrows", "the return is Difficult", "the
|
||
return is aimed at −30%". No code has ever touched it. So the phase whose entire job
|
||
is *getting out with it* had no consequence of its own, and whoever ran it had to
|
||
invent one — and improvising at that moment is exactly how a phase that should cost
|
||
you the trip home ends up costing lives.
|
||
|
||
The extraction now spends that clock instead of hurting anybody:
|
||
|
||
critical out clean, and the window opens back up (clock −1)
|
||
special out clean (clock 0)
|
||
success out (clock 0)
|
||
failure out, but not cleanly (clock +1)
|
||
fumble you do not get out this attempt (clock +1)
|
||
|
||
0 steps → 0%
|
||
1 step → −10% on every Transposition
|
||
2 steps → −20%, and the return is Difficult
|
||
3 steps → −30%, and the return is Difficult
|
||
|
||
`returnCrossing()` reads it alongside the classification modifier and the Coherence
|
||
band penalty it already applied, so the cost lands where this setting says the cost of
|
||
a case belongs — on the crossing home — and the players can see it coming, printed on
|
||
the phase card the moment it happens.
|
||
|
||
Three rules, ten spot-checks. 47 rules, 158 formulas.
|
||
|
||
**Also fixed while in there:** the leg carried no `kind`, so the extraction check
|
||
matched on the terrain PROSE and worked only because the string happens to begin
|
||
"Extraction —". Legs now store their kind, with the prose match kept as a fallback for
|
||
case files generated before today.
|
||
|
||
Verified in play on PLAIN HARBOUR: extraction failed at 47%, the card read *"You are
|
||
out, but not cleanly. The window has narrowed... The way home: −10% on every
|
||
Transposition roll"*, the clock went to 1, and all four crossings took −10%. Nobody
|
||
lost a hit point.
|
||
|
||
## A briefing, and a millennium of ironmongery
|
||
|
||
**The players now get a briefing, not a memo.** The generator was already writing the
|
||
timeline, the cast and two lines of texture that are the best prose in the whole
|
||
document — and filing every word of it behind the GM screen. A new PLAYERS page:
|
||
|
||
- a read-aloud opening built from the classification, the place and the window;
|
||
- **what it is like there** — the texture, with the note that none of it is in the
|
||
file, because the file was written by somebody who was not there;
|
||
- **what is known so far** — the timeline as a table;
|
||
- **who is there** — the cast by name, role, and what they want from you, but never
|
||
what they lie about, which stays on the GM page where it belongs;
|
||
- and a short "before you go" that tells a new player the three things this game
|
||
expects of them: sign for your kit, bring somebody who can make an arrangement,
|
||
keep your anchor on you.
|
||
|
||
Ownership is now **declared by the page** rather than guessed from its index — the
|
||
old `i === 0 ? 2 : 0` would have silently hidden any second player page. Two player
|
||
pages, four GM pages, 300 seeds checked clean.
|
||
|
||
Two bugs in my own prose, caught by reading the output: `classification.texture` is a
|
||
LIST and was being printed comma-joined into a sentence, and the "since then..."
|
||
line was reaching for the last timeline entry, which is *the team crosses* — so every
|
||
briefing ended "since then it has not stopped, and the team crosses."
|
||
|
||
**The historical range.** The department has been running since before it had a name
|
||
and has never thrown anything away.
|
||
|
||
- **38 historical weapons** (from 6): arming sword, longsword, falchion, war hammer,
|
||
mace, military flail, poleaxe, halberd, billhook, pike, quarterstaff, dirk, longbow,
|
||
arbalest, sling, javelin, rapier, main gauche, matchlock, wheellock, blunderbuss,
|
||
boarding pike, cavalry sabre, smallsword, cutlass, Brown Bess, Baker rifle, socket
|
||
bayonet, Martini-Henry, Webley, Lee-Enfield, trench club — each with its period
|
||
stated, because a GM handing out a matchlock should know it is a matchlock.
|
||
- **14 historical armours** (from 2): gambeson, maille shirt, coat of plates, cuirass,
|
||
full harness, buff coat, sallet and bevor, lobster-tail helmet, jack of plate,
|
||
cuirassier armour, Brodie helmet, flak jacket. Weight is the point — full harness is
|
||
9 AP and 25 kg, and the free-carry rule does the rest.
|
||
- **Five new skills** to hang it on: Melee Weapon (Polearm), Melee Weapon (Flail),
|
||
Missile Weapon (Sling), Missile Weapon (Thrown), and **Firearm (Muzzle-loader)** —
|
||
because a muzzle-loader is not a rifle and loading it is the skill.
|
||
- **275 icons**, all generated; every weapon maps to a real skill and every item has
|
||
art, both checked.
|
||
|
||
Catalogue is now **206 items**: 118 gear, 62 weapons, 25 armour, 14 vehicles — 145
|
||
modern, 77 antique, 34 future, 19 anomalous.
|
||
|
||
## GLASS LANTERN — the historical gear in play
|
||
|
||
A village with two war memorials, Hollow class, salvage era **antique**. Antiquities,
|
||
Containment, Rigging, Physician.
|
||
|
||
Salvage from a Hollow world did exactly what the era tag promises: a **poleaxe, a
|
||
chirurgeon's roll, a lodestone and a pocket watch**. They armed themselves out of the
|
||
village — poleaxe to the rigger, a Brown Bess and a maille shirt to the Antiquities
|
||
Officer, a halberd to Containment, a sling to the Physician.
|
||
|
||
Three things the play-through proved, all of them the right behaviour:
|
||
|
||
**Salvaged historical weapons are not automatically usable.** The poleaxe, halberd and
|
||
sling all came up at **5%**, because Polearm and Sling are skills nobody on the team
|
||
has. The Rigging Officer knows her boarding axe at 68% and a poleaxe at 5% — the same
|
||
shape of weapon, a different skill, and she sensibly kept the axe. Salvage is a
|
||
promise of usefulness, not a free upgrade.
|
||
|
||
**Historical armour makes a knife pointless.** The Antiquities Officer in maille over
|
||
brigandine came out at AR 8, and the village hit her four times with utility knives
|
||
for **0, 0, 0 and 0 damage**. 1d4+2 against eight points of armour is nothing, and
|
||
that is exactly the historical fact the numbers should produce. She was also
|
||
**overloaded at 28 kg against a 20 kg free carry**, which is the other half of the
|
||
same fact.
|
||
|
||
**Muzzle-loaders behave.** Flintlock and Brown Bess both hold one shot with a
|
||
two-round reload. She fired the flintlock in round one, and thereafter fought with the
|
||
duelling sword rather than spend two rounds reloading in a melee — which is the
|
||
decision the rule is supposed to produce.
|
||
|
||
The fight ran seven rounds. The village lost one dead and two dying; the team took
|
||
almost nothing, because they were wearing four hundred years of armour development and
|
||
the village had knives.
|
||
|
||
**R-226 · "Tradecraft first" was too literal.** My own R-221 fix overshot. A
|
||
Containment Officer whose Tradecraft had caught a stray bonus point sat at 12% against
|
||
a printed base of 5 — technically trained — and led every phase ahead of a Rigging
|
||
Officer on **53% Navigate**. The preference is meant to mean "whoever reads a
|
||
developing situation best", not "anybody with a point in the right column". Tradecraft
|
||
still wins ties and wins when it is genuinely better; it no longer wins when it is
|
||
plainly worse. The Containment phase went from 6% to 26% on the same team.
|
||
|
||
## An overnight build: hazards, automatic fire, two weapons, a bestiary, and space
|
||
|
||
### Stress test first
|
||
150 characters — every one of the 25 postings, at Probationary and Case Officer, in
|
||
all three species — checked against fourteen invariants each. Nothing broken. Then the
|
||
edges: characteristics at 1, 3 and 21; every location destroyed at once; twelve pikes
|
||
carried; hit points set to −99. All degraded gracefully except one.
|
||
|
||
**R-227 · a destroyed head was a nap.** The printed effect has always read "For
|
||
anything that keeps its brain there, this is death" and the code set `unconscious` and
|
||
stopped. An agent with every location on their body destroyed, head included, reported
|
||
as DOWN. `destructionOutcomeFor()` now says what destruction means: a head is death, a
|
||
vital or sensory location drops you to zero, a limb is ruinous and survivable.
|
||
|
||
### The ways the world kills you
|
||
- **Falling** — a die per 3 m, random location, **armour does not apply** because the
|
||
ground is not a weapon. Soft landing halves the distance; a controlled descent
|
||
halves it again. Verified: 12 m killed an agent outright; the same fall controlled
|
||
left them on 2 hit points.
|
||
- **Drowning** — CON rounds with warning, half without, then a point a round that does
|
||
not stop. Runs straight past the Major Wound threshold to zero and into the dying
|
||
count. Verified over 24 rounds.
|
||
- **Fire** — a condition with a size, not a hit. Worn armour helps on the round you
|
||
CATCH and never after, because by then the fire is inside it.
|
||
- **Explosives** — one roll at the centre, shared by band, and **cover beats armour**:
|
||
armour counts at contact only and only half. Verified on four people at 1 m, 4 m,
|
||
10 m and 40 m: 17, 3, 0, and outside it entirely.
|
||
|
||
### Automatic fire, and two weapons
|
||
One roll for a burst, and the **success level** says how much of it arrived — the same
|
||
band machinery as everything else rather than four rolls and a stalled table. **The
|
||
rounds are spent either way.** Verified: a failed full-auto burst spent ten rounds for
|
||
nothing, which is the honest part of the rule.
|
||
|
||
Two weapons costs −20/−40 untrained and 0/−20 with the new **Ambidextrous** talent.
|
||
Nothing removes the off-hand penalty, and the off hand takes the lighter weapon at no
|
||
more than 2 kg — the rule that refuses two poleaxes, tested and refused.
|
||
|
||
### The bestiary
|
||
Twenty-one entries in three families, each with characteristics, natural armour, a
|
||
tactics line and a POWER written as a rule with a cost rather than a special ability.
|
||
FOLKLORE: barghest, kelpie, redcap, boggart, church grim. HORROR: hollow man, the
|
||
tenant, the choir, the carrion file, the understudy. FORWARD: surveyor, quarantine
|
||
unit, cuckoo array, long walker, the auditor. Every skill and weapon reference checked
|
||
against the catalogue.
|
||
|
||
### Space, and everybody in it
|
||
Eight STATIONS, each using a skill somebody already has, so the Physician and the
|
||
Translation Officer have a chair. Range is a band; the helm moves one a round, in or
|
||
out. **Heat is the pressure** and it is the whole design.
|
||
|
||
Two rounds of tuning, both from playtest and both necessary:
|
||
- First cut had every station heating and one engineer venting. Heat reached the
|
||
ceiling by round four and locked the helm — a countdown, not a decision. Now only
|
||
Gunnery is charged for trying (the shot left either way, exactly as the burst rule
|
||
reasons), looking is free, and venting sheds 3.
|
||
- Second cut was too cold — heat sat at 0 and nobody cared. Settled between.
|
||
- **The helm only ever closed.** Every fight ended docked with no way to break off
|
||
short of the anchor. The helm now chooses in or out.
|
||
|
||
Final playtest: heat 2 → 5 → 3 → 5 → 7 → 10, and at the ceiling the helm locked out
|
||
because the crew kept shooting instead of cooling. That is the decision the subsystem
|
||
is for.
|
||
|
||
64 rules, 198 spot-checked formulas, a 12-page rulebook in reading order.
|
||
|
||
### The Codex pass, and what it cost me
|
||
|
||
Put the night's five subsystems to Codex with the instruction to be blunt. It found
|
||
three things that were genuinely wrong and I have fixed all three.
|
||
|
||
**R-228 · Ambidextrous was a compulsory build.** At 0/−20 the talent gave a
|
||
full-strength main hand AND a cheap second attack — near-free action economy, and
|
||
therefore the only sane pick for anybody who could hold two things. The 2 kg limit
|
||
stops poleaxes, not optimisation with a pistol and a knife. Ambidextrous now buys
|
||
CONSISTENCY rather than a free attack: **−20 in both hands**, against −20/−40
|
||
untrained. Still plainly worth having; no longer mandatory.
|
||
|
||
**R-229 · full auto was a firing squad.** Ten independent hits off one critical, each
|
||
with its own damage roll, hit location and Major Wound check. Against a hard target it
|
||
was the correct answer to everything. Hits are now **capped at 3/5/7** rather than
|
||
proportional to the burst, and however many rounds connect, **a burst is one wounding
|
||
event** and rolls the Major Wound table at most once.
|
||
|
||
**R-230 · Engines deleted the pressure it was supposed to create.** Guns +2 and Helm
|
||
+1 against a vent of 3 made a standard fighting round heat-NEUTRAL — the craft cooled
|
||
while it fought, and Heat only existed when the engineer failed or was absent. This
|
||
is the same fault as my earlier over-correction, in the other direction, and my own
|
||
second playtest had shown it sitting at 0 for four rounds without my drawing the
|
||
conclusion. Venting is now 2/3/4, so a shooting round costs +1 net.
|
||
|
||
Two more taken as written:
|
||
- **Falling halving stacked.** Soft ground and a controlled descent both halved, so
|
||
together they quartered — the same mitigation counted twice. They no longer stack.
|
||
- **Blast ordering was undefined.** Flooring at each step meant the answer depended on
|
||
the order the functions were called in. One formula now, floored once: rolled x
|
||
range share x cover, rounded down, and only then armour.
|
||
|
||
**And the sharpest observation, which I acted on structurally:** "giving everyone a
|
||
roll is not the same as giving everyone a decision." Sensors, Damage Control and
|
||
Medical were seats that rolled dice while the gunner decided the fight. Each now has
|
||
mutually exclusive modes — Sensors marks, spoofs or reads; Damage Control patches or
|
||
BRACES so the next hit lands at half; Medical restores a crewman or STEADIES the ship
|
||
so the heat penalty does not apply that round. Bracing and steadying both bite in the
|
||
engine, not just on the card.
|
||
|
||
Not taken tonight, recorded as owed: station fumble tables, Signature as a live
|
||
mechanic, mark duration and stacking, hull and system criticals, extinguishing fire,
|
||
rescue from drowning, and an explicit interaction table for hazards against armour,
|
||
Major Wounds, bleeding, zero and dying. Codex is right that these are implied; they
|
||
are a session's work, not a paragraph's.
|
||
|
||
## SALT COUNTY — the bestiary in play
|
||
|
||
A county records office over a bank, and a member of the public behaving as somebody
|
||
who died eleven days ago. Field Lead, Physician, Night Warden, Antiquities Officer.
|
||
|
||
The generator set this up better than I could have. The brief names a **revenant** —
|
||
which is in the bestiary already — and the truth page says the cause is *elsewhere in
|
||
the same location and has not been noticed*. In a records office, that is the
|
||
**carrion file**.
|
||
|
||
**The revenant was resolved without a shot.** Its printed leverage is "tell it where
|
||
that person is, and it will go, and that becomes somebody's problem" — the Antiquities
|
||
Officer made the Knowledge roll off the parish register and it left. That is a
|
||
statblock's tactics line doing the work a fight would otherwise have done.
|
||
|
||
**Then the gap surfaced, and the file did what it is for.** AMENDMENT is written as a
|
||
rule with no saving roll, and that is deliberate: it removed the Antiquities Officer's
|
||
Language (Trade Cant) at 63%, the Night Warden's at 79%, and the Field Lead's Dead
|
||
Reckoning talent — permanently, with no roll offered and nothing to roll against. The
|
||
horror of that entry is entirely in the absence of a defence, and it reads correctly
|
||
at the table.
|
||
|
||
**Burning it exercised the new fire rules on a thing rather than a person.** Armour 6
|
||
counted on the round it caught and never again: 3, then 6, then 11, and the file was
|
||
at zero in three rounds. The team were standing in the same burning archive and took
|
||
the "clothes alight" band — the Antiquities Officer's armour 4 absorbed it completely,
|
||
the Night Warden's 2 did not.
|
||
|
||
The case closed at Containment 8, Resources 1, Clearance 5. A failed extraction
|
||
advanced the window clock to 1, and every crossing home took the resulting −10%, which
|
||
is the retuned extraction rule working end to end.
|
||
|
||
**One honest note on my own reporting:** a probe line read "Dead Reckoning: gone" for
|
||
all four agents. Only the Field Lead ever had it — the other three had never been
|
||
awarded it, and the probe could not tell the difference between a talent removed and a
|
||
talent never held. The amendments actually made were three, not four.
|
||
|
||
## Fire modes, fitted kit, and a rail instead of a strip
|
||
|
||
Four requests, and the first of them turned over the usual stone.
|
||
|
||
**R-231. Every weapon has carried a fire mode since the packs were first built, and
|
||
nothing has ever read it.** A musket could rip a ten-round burst; "rapid" bought a
|
||
weapon precisely nothing. `FIRE_MODE` now gates the burst rules and carries its own
|
||
attack modifier, so the mode is a decision rather than a label: **slow** +10% and then
|
||
you work for the next one, **single** the baseline, **fast** a second shot at −20% to
|
||
*both*, **rapid** bursts, **beam** +5% and it can be held. The gate is checked inside
|
||
`burstAttack` rather than only in the dialog, so no macro or harness can route round it.
|
||
|
||
**R-232. `fast` was not a fire mode at all.** Five weapons carried it — longbow, sling,
|
||
javelin, the Webley and the Lee-Enfield — and the system's list of modes did not
|
||
include it, so the item sheet's dropdown had no matching option and re-saving one of
|
||
those weapons silently blanked its mode. The list was hand-written next to the rule
|
||
instead of derived from it. It is derived now, and the guard that should have caught
|
||
this was the one I had to fix in R-236.
|
||
|
||
**R-233. `burstAttack` was reachable from nothing.** The automatic-fire rules shipped
|
||
complete and could not be used from any sheet. It is now the resolution path the fire
|
||
selector chooses, and it takes the dialog's whole modifier stack rather than rebuilding
|
||
its own — the first version charged the player twice for the same burst.
|
||
|
||
**R-234. Any weapon with a Coherence cost could not be attacked with at all.** The
|
||
handler read the weapon, awaited the Coherence charge, and then read the weapon
|
||
*again* — but `currentTarget` is only set while an event is being dispatched, so the
|
||
second read returned null and the attack died on a TypeError with nothing shown to the
|
||
player. The Redaction lance, the Unmaker and every piece of transposed materiel were
|
||
unusable, and the failure was completely silent. Resolve once, before anything is
|
||
awaited.
|
||
|
||
**R-235. The damage card was the last one wearing Foundry's stock dice styling.** It
|
||
posted a bare Roll and let the core template dress it — grey boxes on a white panel in
|
||
the middle of a navy sheet. It renders through the module's own card now, with the Roll
|
||
still riding on the message so the dice stay inspectable and Dice So Nice still
|
||
animates.
|
||
|
||
**R-236. The duplicate-rule guard reported an honest table as a copy.** `FIRE_MODE` has
|
||
`slow` and `fast`; so does the entirely unrelated `speedBands` vocabulary, and two
|
||
shared key names was enough to fail the build. Every real instance this guard has ever
|
||
caught duplicated the *whole* table, so it now wants a majority of the keys — and
|
||
prints partial overlaps rather than swallowing them.
|
||
|
||
**Sights and lamps.** A sight is not a flat bonus, which is why most systems end up
|
||
with everybody carrying the same scope. It is graded by range band, so **a telescopic
|
||
sight is a −10% liability inside ten metres** and worth +20% at extreme, and it only
|
||
counts if it *fits* — the scope and the thermal sight are rifle furniture. Because the
|
||
band is already known by the time the modifier is wanted, the sight applies itself:
|
||
there is nothing to select and nothing to forget. The dark now costs something, a
|
||
carried lamp buys it back to dim and never to daylight, and the card says plainly that
|
||
the lamp shows them exactly where you are.
|
||
|
||
**R-237. "Automatic" that needs a re-import is not automatic.** Agents created before
|
||
those fields existed carried a Field lamp whose `lightSource` was the template default
|
||
of false — the lamp sat in the pack doing nothing. A migration stamps fitted gear from
|
||
the compendium on load, matched on `flags.ringbrp.key` rather than on display name,
|
||
because the fuzzy-name match this project used before once mapped a climbing kit to
|
||
breaching charges.
|
||
|
||
**My own error, caught by playing it.** I first costed a sustained beam as
|
||
`multiplier × energyPerShot`, which made a locked beam cost 60 charge from a 20-charge
|
||
cell — unfirable, and the failure was a warning the player would read as a bug. The
|
||
magazine is spent one per shot everywhere else in the system, so the cost is in shots:
|
||
a locked beam burns six. Worth noting that `energyPerShot` itself is written by the
|
||
pack builder and **read by nothing** — the same defect again, left alone here because
|
||
wiring it would change every energy weapon's economy.
|
||
|
||
**The tabs.** Eight of them across the top of an 820px sheet compressed to 11px type
|
||
and were genuinely hard to read. They are a rail down the side now: one line each,
|
||
larger type, a solid block for the active tab, and counts as badges. The sheet is 152px
|
||
wider to pay for the rail so nothing else got tighter, and the skills list is driven by
|
||
the space actually available rather than by a window-width breakpoint that was
|
||
measuring the wrong box.
|
||
|
||
Also noted, not fixed: fourteen rules are imported into `ringbrp.mjs` and read nowhere
|
||
in it. They are live rules with real readers in the rulebook generator and the guards,
|
||
so this is dead *imports* rather than dead rules — but it is the same smell.
|
||
|
||
## THE GOODS YARD — the four fire modes, side by side
|
||
|
||
One agent per mode, all four trained to exactly 60%, so the fight reads the mode and
|
||
not the shooter. A barghest in a coal yard at 02:40, pitch dark, one lamp between
|
||
four of them. Four rounds. The dog left at 4 hit points of 16 and nobody caught it.
|
||
|
||
**The modes separated cleanly, and not in the way I expected.**
|
||
|
||
**Slow was the best weapon in the fight.** Dunn's Baker rifle hit on both the shots it
|
||
got, and the +10% is what bought them: round one at 35% he rolled 33, and at the 25%
|
||
he would have had without it that is a miss. The reload is a real cost — he was out of
|
||
the fight for rounds two and three and the dog reached him — but a mode that turns a
|
||
33 into a hit twice is not the weak option.
|
||
|
||
**Fast is not two chances to hit. It is a second defence at a penalty.** Rakhi's two
|
||
Webley shots in round two both landed at 20%, and the first was dodged clean. The
|
||
second one hurt precisely because the barghest had already spent its free defence and
|
||
had to dodge again at −30%. That is the mode's actual function and it is better than
|
||
the tooltip suggests.
|
||
|
||
**Rapid was the worst thing in the yard.** Ceyla fired 20 of 30 rounds across four
|
||
rounds of combat and hit nothing at all. A full magazine at −30%, in the dark at −30%,
|
||
is a 1% roll that still spends ten rounds — and the rules charge her honestly for
|
||
every one. This is not obviously wrong; automatic fire into a dark coal yard at
|
||
seventy metres *should* be futile. But it means **the burst options are close to
|
||
unselectable in any fight where the light is bad**, and the light is bad in most of
|
||
this game. Flagged rather than tuned: I would rather see it in a second fight before
|
||
touching the numbers.
|
||
|
||
**Beam did the damage.** The locked beam landed for 10 — 1d4+6 with its 2d6 riding on
|
||
the standing blow exactly as designed — and took the barghest from 11 to 4 through
|
||
armour 3. It cost 6 charge a shot and, because the lance is transposed materiel, **1
|
||
Coherence every time Tomas pulled the trigger**. He ended the fight Loose at 6, with 2
|
||
charge left. That is the most interesting economy in the fight and nothing needed
|
||
changing to produce it.
|
||
|
||
**The sight graded itself the way it was meant to.** Ceyla's telescopic sight was +5%
|
||
at seventy metres and **−10% once the thing was inside twenty-five**, and she felt it
|
||
both times. Nobody had to remember it.
|
||
|
||
**The lamp is the finding.** Tomas's Field lamp turned pitch dark into dim for him — a
|
||
40-point swing, the largest single modifier in the fight — and the card told him each
|
||
time that it showed the barghest exactly where he was. Which it did: the dog went for
|
||
the lamp in round two. But **a lamp only lights its carrier**. Three agents stood
|
||
inside the same pool of light at −50% while the fourth shot at −10%. That is wrong at
|
||
the table, and it is the one design change this fight actually argues for.
|
||
|
||
### Defects the fight surfaced
|
||
|
||
**R-238. A fast action fired twice and paid for one round.** The Webley put two shots
|
||
out and the cylinder went down by one. Ammunition was charged once before the shot
|
||
loop instead of once per shot — the exact dishonesty the burst rules were written to
|
||
avoid, and the mode that ought to cost the most was the only one getting a free shot.
|
||
Charged per shot now, and the loop stops when the weapon runs dry mid-action.
|
||
|
||
**R-239. `morale.downThreshold` is written by the pack builder and the template and
|
||
read by nothing.** Every NPC in the game carries a morale field and there is no morale
|
||
rule. Whether the barghest broke off was mine to decide with no support from the
|
||
system, which is fine as a GM call and not fine as a shipped field. Not fixed — it
|
||
needs a rule, not a patch.
|
||
|
||
**Two of my own, worth recording because the pattern keeps repeating.** I did
|
||
arithmetic on `system.coherence` (an object) instead of `.value` and reported a
|
||
Coherence loss that never happened; and I read `system.hitPoints`, which does not
|
||
exist — the field is `system.hp`. Both were caught inside a minute by printing the
|
||
result instead of trusting it, which is the only reason this log is accurate.
|
||
|
||
**A content wart:** the barghest bites with something called "Vesh grasp limb" — the
|
||
generic `grasp` weapon still carrying a Ringworld-era display name. Every creature in
|
||
the bestiary that uses `grasp` inherits it.
|
||
|
||
## R-240. A lamp lights the people standing in it
|
||
|
||
The goods yard fight made the case: three agents inside the same pool of light shot at
|
||
−50% while the fourth, four metres away, shot at −10%. Nobody would run that at a
|
||
table, and the rule was wrong rather than merely coarse.
|
||
|
||
A lamp now has a **reach**, and it is per item rather than global, because a weapon
|
||
lamp and a vehicle searchlight are not the same offer — 6 m, 10 m for the issue field
|
||
lamp, 4 m for cold light strips, 30 m for a roof searchlight. Anyone inside the reach
|
||
is in the light, and the card names whose lamp it is: *"MODE: Tomas's, 4 m away — you
|
||
are in it."* Both halves of the rule travel together, so the pool is one position and
|
||
the whole group is standing in it.
|
||
|
||
Measured from the tokens, because the canvas already knows who is standing where. The
|
||
same shot that started this — Rakhi, no lamp of her own, four metres from Tomas's, in
|
||
pitch dark — went from **20% to 60%**, and Ceyla twenty-five metres out still ate the
|
||
full −50%.
|
||
|
||
**The interesting part was the unknown.** `Number(null)` is 0, so an unmeasured
|
||
distance read as *standing on top of the lamp* and would have lit any scene with no
|
||
tokens placed. My own spot-check caught it before it ever ran: an unknown distance is
|
||
now explicitly not lit, and the dialog carries a tick that arrives pre-set when the
|
||
canvas could measure it and is the GM's answer when there is nothing to measure. That
|
||
keeps it automatic where it can be and honest where it cannot, rather than quietly
|
||
lighting the world.
|
||
|
||
Note for deployment: `template.json` is read when Foundry loads the system, so the new
|
||
`lightReachM` field did not exist on items until the server was restarted — the
|
||
migration then stamped it. A remote server needs the restart, not just a pull.
|
||
|
||
## The combat tab, reordered — and R-241, a weapon nobody could ready
|
||
|
||
**R-241. There was no way to ready a weapon.** The strip under the characteristics
|
||
reads `equipped`, and the only `toggle-equipped` control on the whole character sheet
|
||
was on the *armour* list. A weapon could only be readied by opening its item sheet and
|
||
finding the tickbox there — which is why the strip on most sheets showed whatever the
|
||
pack builder happened to have equipped and nothing else. The weapons table now carries
|
||
the same toggle, the row is tinted when it is readied, and a line above the table says
|
||
plainly what the control does.
|
||
|
||
**R-242. The off state did not look off.** `.ringbrp-sheet a` is more specific than a
|
||
bare `.rb-equip`, so an unequipped toggle was painted white and read as lit. That was
|
||
true of the armour toggle too, and had been since the toggle was added.
|
||
|
||
**The tab now runs in the order it is used**: weapons, armour, defend, reaction, and
|
||
the body last. Weapons lead with 40px art and a display-face name, because they are
|
||
what the tab is for and they were previously below two screens of anatomy and reaction
|
||
arithmetic.
|
||
|
||
**The figure is a partial now.** It is drawn in three places — the header thumbnail,
|
||
the combat tab and the anatomy tab — and it was previously twenty lines of inline
|
||
Handlebars that existed once. Three copies of that would have been the template-layer
|
||
version of the defect `check-rules` exists to prevent: a location present in one
|
||
drawing and missing from another. One partial, registered at init, three call sites.
|
||
|
||
The header now carries a **34px figure and every location with its damage and the
|
||
armour over it**, so the commonest question at the table — how much armour is on the
|
||
part that just got hit — is answered on the main page without changing tab.
|
||
|
||
Noted while working: the mini figure inherited `.rb-body-svg { max-width: 180px }`
|
||
from further down the stylesheet and blew the header out to 385px tall. Same-specificity
|
||
selectors, later one wins; the compound `.rb-body-svg.rb-body-mini` fixes it. Worth
|
||
remembering that this stylesheet is ordered by section rather than by specificity.
|
||
|
||
## R-243. A helmet was protecting your legs
|
||
|
||
Asking for "armour and the type of armour" on each location turned out not to be a
|
||
display request. Every equipped piece added its points to **every** location, so a
|
||
Brodie helmet protected your shins and a cuirass protected your skull. With a
|
||
catalogue that contains sallets, lobster-tail helmets and three separate cuirasses
|
||
that is not a simplification, it is wrong — and it could not be printed on the sheet
|
||
without the sheet saying something visibly false.
|
||
|
||
Armour now declares **coverage**, as a named set rather than a list of locations,
|
||
because a piece has to fit whatever body is wearing it: `all`, `torso`, `torsoArms`,
|
||
`torsoLimbs`, `head`. The sets expand to location *kinds*, so a cuirass covers the
|
||
torso of a baseline agent and the trunk and ridge of a vesh without either the item or
|
||
the wearer knowing the other exists. Unstated coverage is `all`, so anything that does
|
||
not declare behaves exactly as everything did before.
|
||
|
||
Eighteen of the twenty-five catalogue pieces are not whole-body and now say so.
|
||
|
||
**The trap was double counting.** `hitLocationRoll` subtracted `sys.armourPoints +
|
||
loc.armour` — the global worn total plus the body's own armour at that location.
|
||
That was correct while armour was global and becomes a double count the moment the
|
||
location knows its own pieces. It now reads the location's total and nothing else,
|
||
checked by firing fourteen hits of 10 damage at every location and asserting the
|
||
damage applied equals `10 − that location's armour` each time. All fourteen matched.
|
||
|
||
**A single ARMOUR number no longer exists**, so the header shows the torso — which is
|
||
what the question means, and where most hits land — with `0–8 by location` beside it
|
||
when the rest of you is not that. Worked example from the verification: stab vest,
|
||
cuirass and a Brodie helmet gives 8 on the torso, 3 on the head and **0 on both arms
|
||
and both legs**, where before it was 11 everywhere.
|
||
|
||
**This is a real difficulty change and it is meant to be.** Agents in partial armour
|
||
are markedly more fragile than they were an hour ago — limbs are frequently bare, and
|
||
limbs are 12 of 20 on the melee table. Anyone who wants the old behaviour sets every
|
||
piece to `all` on its item sheet.
|
||
|
||
## Coherence, promoted
|
||
|
||
Moved out of the derived strip and into the vitals at the top right, at the same size
|
||
as hit points, with its band under it and the box itself changing colour as it falls —
|
||
yellow at Adrift, red at Unmoored and Displaced. It is the number that measures whether
|
||
an agent is still filed as themselves, and it was set in 11px grey between MOV and
|
||
carried weight.
|
||
|
||
## THE CHANTRY RUIN — coverage, measured
|
||
|
||
The point of this fight was to make coverage the only variable. Two officers built
|
||
from the same pregen, both 12 hit points, both Sword 55% and Dodge 45%, and armour
|
||
chosen so that **the one carrying more points was carrying it in fewer places**:
|
||
|
||
* **Suited** — Breaching suit. **7 points, on all seven locations.**
|
||
* **Plated** — Cuirass and a Brodie helmet. **9 points**, on three.
|
||
|
||
Against a redcap: Polearm 80%, Dodge 75%, a halberd at 1d10+2, and POWER: NOT TIRED,
|
||
which ignores the cumulative defence penalty so every parry it makes is at full skill.
|
||
In melee **fourteen of the twenty hit locations are limbs**, which is the whole test.
|
||
|
||
**Plated's own armour worked perfectly, and it did not matter.**
|
||
|
||
| Round | Where the halberd landed | Armour there | Rolled | Through |
|
||
|---|---|---|---|---|
|
||
| 1 | right leg | 0 | 5 | **5** |
|
||
| 3 | abdomen | 6 | 6 | 0 |
|
||
| 4 | head | 3 | 4 | 1 |
|
||
| 5 | right leg | 0 | 10 | **10** |
|
||
|
||
Four connecting blows. The cuirass and the helmet between them stopped fifteen points
|
||
of damage. The two that found a bare leg did every point they rolled, destroyed the
|
||
leg outright (12 damage against a location maximum of 6), took Plated to zero and
|
||
started a six-round dying clock with a bleeding Major Wound. **Under the old rule that
|
||
leg carried 9 points and both blows would have done nothing at all.**
|
||
|
||
**Suited took the same weapon on a limb and felt nothing.** Round 6, left arm, 3
|
||
rolled against 7 — zero through. Round 2 and 8 and 9 turned aside on the dodge. After
|
||
nine rounds against an 80% attacker they were still at 12 of 12.
|
||
|
||
**And then full coverage turned out not to be immunity.** Round 10 the halberd came in
|
||
on a **critical**; Suited dodged *successfully* and a success against a critical only
|
||
reduces it one step, so it landed as a special — half armour — and 12 damage put 9
|
||
through a 7-point suit. Round 11 finished them. Zero to zero in two rounds, with a
|
||
catastrophic Major Wound.
|
||
|
||
**What the fight says about the change.** Coverage is now the single most important
|
||
thing about a suit of armour, far more than its points: 9 points in three places lost
|
||
a leg and nearly a life, 7 points in seven places went nine rounds untouched. That is
|
||
the correct shape and it is a large difficulty swing for anyone in partial armour —
|
||
which, in a catalogue with this many cuirasses and helmets, is most people. Critically,
|
||
it did not make full armour boring: a critical halves it and a maximum-damage halberd
|
||
still gets through.
|
||
|
||
Saved by the rule that has been quietly working since R-223: First Aid at 37% failed
|
||
on the first attempt and landed on the second, staunched the bleeding, healed 3, and
|
||
`reviveIfHealed` took Plated out of the dying state without being asked. My harness
|
||
then called `tickDying` on a revived character and read `.state` off the null it
|
||
correctly returned — the rule was right and the probe was wrong.
|
||
|
||
## Shields — armour with a switch
|
||
|
||
A shield is the one piece of armour you *hold*, and it protects you exactly as much as
|
||
you are currently holding it up. Everything interesting follows from that switch
|
||
having a cost, so the design is: raised it is armour and a defence, lowered it is
|
||
weight, and the price of keeping it up is that it occupies a hand and you are shooting
|
||
around it.
|
||
|
||
* **Raised** — its points apply to what it covers, and it is the only armour you can
|
||
also defend with, on a new **Shield** skill.
|
||
* **Lowered** — nothing. Verified: heater shield slung reads 0 on every location,
|
||
raised reads 4 on torso and both arms and 0 on legs and head.
|
||
* **The cost** — −20% to your own ranged attacks while it is up, printed on
|
||
the card as plainly as anything else: *"Heater shield −20%"*. That is what stops
|
||
"always raised" being the only answer; it is obviously right for a containment
|
||
officer and obviously wrong for a marksman.
|
||
* **Only one goes up.** Raising a second lowers the first. Two shields is not a stance.
|
||
|
||
Seven of them, from a 2-point buckler to a 6-point ballistic shield, with the pavise
|
||
and the barrier plate covering legs as well because they are walls you carry.
|
||
|
||
**`degradation` finally does something.** It has been on every armour item since the
|
||
packs were first built and was read by nothing. Shields wear: a blow costs the shield a
|
||
point for every step the attack stood above an ordinary success, so an ordinary hit
|
||
costs it nothing, a special costs 1 and a critical costs 2. At its limit it comes apart
|
||
— no points, nothing to parry with, and it drops out of your hand and refuses to be
|
||
raised again.
|
||
|
||
### Two defects found by testing it rather than reading it
|
||
|
||
**R-244. Every shield defence quietly rolled Dodge.** I added `"shield"` as a third
|
||
defence type and passed it through `defenceTypeAllowed`, which returned it unchanged —
|
||
so `kind` was `"shield"`, neither of the `kind === "parry"` branches matched, and the
|
||
function fell through to the dodge branch. It was invisible in the chat card and only
|
||
showed up as a statistical smell: eight rolls at a supposed 85% produced six failures.
|
||
Taking a blow on the shield **is** a parry — it locks with parries and is not a dodge —
|
||
so the shield is a flag on a parry rather than a type of its own.
|
||
|
||
**R-245. The wear rule was unreachable.** I first wrote it to fire only when the shield
|
||
turned a blow aside *completely*. But under graded defences only a critical fully stops
|
||
a critical, so a shield would have gone its entire service life without a scratch. Wear
|
||
is for what the shield **absorbed**: a shield that drags a critical down to a special
|
||
has very much taken that blow, and that is exactly when shields splinter. Ten forced
|
||
criticals produced zero wear under the first rule; under the corrected one the shield
|
||
went 0 → 2 → 4 → broken in three absorbed blows.
|
||
|
||
Both were mine, both were introduced in this change, and both were found by driving the
|
||
real functions rather than by reading the diff.
|
||
|
||
## THE BELL TOWER STAIR — shields, and what they cost
|
||
|
||
Two officers, identical kit — riot shield, stab vest, 12 hit points, Shield 65% — and
|
||
one difference: the **Warden** holds the shield up on the stair, the **Marksman** keeps
|
||
it slung and shoots. A redcap with an 80% halberd.
|
||
|
||
**Raised, the shield is most of your armour.** Warden with it up: torso 5, arms 3.
|
||
Marksman with it slung: torso 2, arms 0. The same item, the same character.
|
||
|
||
**An ordinary blow turned aside costs a shield nothing, and that is the right call.**
|
||
Rounds 1 to 5 the Warden parried four halberd blows on the shield and it finished
|
||
without a mark. Had every parry cost a point, a 5-wear riot shield would have been
|
||
kindling before the fight was properly started.
|
||
|
||
**Round 8 is the rule working.** A **critical** halberd; the shield parry succeeded,
|
||
which under graded defences drags a critical down one step rather than stopping it. The
|
||
shield **took 2 wear for the blow it absorbed** and the reduced blow still landed — on
|
||
the left arm, where the shield's 3 points were, for 7. Warden 8 → 1. That is a shield
|
||
doing exactly what shields do: it did not save him, it made the difference between a
|
||
wound and a killing.
|
||
|
||
**Both officers were killed through the gaps.** A riot shield covers torso and arms.
|
||
Every wound in the fight landed somewhere it does not reach:
|
||
|
||
* Warden — right leg 4, left leg 10, and down at zero, dying.
|
||
* Marksman — **head, 10 damage against a location maximum of 5.** Destroyed. Dead
|
||
outright, not dying, which is R-227's fix holding.
|
||
|
||
**And the ranged penalty decided the fight.** With the Warden down, the Marksman raised
|
||
the shield and kept shooting — at 65% less 20%, so 45%. Four shots: **93, 89, 73, 65.**
|
||
That last one is the whole rule in a single number. **65 is a hit at 65% and a miss at
|
||
45%.** The redcap was on 3 hit points of 13 and the shield cost the Marksman the kill,
|
||
and then the fight, and then his life.
|
||
|
||
The redcap finished on 3 and walked away.
|
||
|
||
**What this says about the design.** The trade is real and it is legible at the table:
|
||
the shield roughly doubled the torso armour of both agents and turned aside five blows
|
||
between them, and it also cost a marksman a creature that was one hit from dead. Wear
|
||
is paced correctly — 2 of 5 on the Warden's shield after a critical and four ordinary
|
||
parries, so a shield lasts a fight and not a campaign. Nothing needed tuning after the
|
||
two defects found while building it.
|
||
|
||
Worth noting for anyone issuing shields: a riot shield is `torsoArms`, and in melee
|
||
**legs and head are 10 of 20**. A shield is not a suit of armour and this fight is what
|
||
that sentence costs.
|
||
|
||
## Riot shields for Containment and the Night Warden
|
||
|
||
Both postings now draw a **riot shield** from stores, and both are trained to use it.
|
||
|
||
Issuing a shield without the skill would have been R-212 again with a different item —
|
||
the Antiquities Officer who drew a flintlock and rolled 5% with it — so
|
||
`melee_weapon:shield` goes into each posting's support list. It is **swapped in, not
|
||
added**: keeping five core and five support means the shield arrives without a free
|
||
power bump, which is not what was asked for.
|
||
|
||
* **Containment** drops `stealth`. A posting whose blurb is "goes through the door,
|
||
holds the corridor" was always the least stealthy thing in the department.
|
||
* **Night Warden** drops `navigate`. They sit with one site all night; they are not
|
||
going anywhere.
|
||
|
||
**check-kits now covers shields.** The guard has always required a posting to be
|
||
trained in the weapons it carries, and a shield is armour you defend *with*, so it
|
||
needs its skill for exactly the same reason. Verified by removing the skill from the
|
||
Warden and watching the build refuse:
|
||
|
||
check-kits: FAILED
|
||
warden (Night Warden): issued Riot shield, trains no melee_weapon:shield
|
||
|
||
**What it does to the two postings.** Four generated agents, shield skill landing at
|
||
25–30% (the support tier), and none of them encumbered — though a Containment officer
|
||
came out at 32.9 of 33 kg, which is as close to the line as this kit should get.
|
||
|
||
| | slung | raised |
|
||
|---|---|---|
|
||
| Containment (breaching suit, `all` 7) | 7 everywhere | torso and arms **10**, legs and head 7 |
|
||
| Night Warden (stab vest, `torso` 2) | torso 2, all else 0 | torso **5**, arms **3**, legs and head 0 |
|
||
|
||
The shield ships **slung**. Raising it is a choice made at the table, which is the whole
|
||
design — and for the Warden it is the difference between two points of armour and a
|
||
real one.
|
||
|
||
## Trades — recruited from an ordinary job
|
||
|
||
Ten of them, and they are not postings. A posting is what the department made of you;
|
||
a **trade** is what you were before any of it, because there is no degree in crossings
|
||
and the department has to recruit somebody. So it recruits a doctor, a joiner, a
|
||
detective sergeant, and puts them through six weeks at the training centre.
|
||
|
||
Construction Worker · Doctor · Professor · Engineer · Law Enforcement · IT Specialist ·
|
||
Scientist · Linguist · Entertainer · Criminal.
|
||
|
||
**A trade character is built in two halves, and the join is the whole point.**
|
||
|
||
* The **trade** is genuine expertise, rated on exactly the same bands a posting's
|
||
skills are. A doctor is a proper doctor: Medicine 55, Biology 59.
|
||
* **INDUCTION** goes on top at **25–40%** — Tradecraft, Anomaly Lore, First Aid,
|
||
Firearm (Pistol), Dodge — plus a sidearm, a vest, cordon kit, a cover identity and a
|
||
ward, on top of standing issue.
|
||
|
||
That band is low deliberately. Six weeks is not a career, and a surgeon recruited last
|
||
year is a far better surgeon than they are a Custodian. Ten generated agents show the
|
||
gap plainly: trade skills land in the high forties and fifties, induction in the
|
||
twenties and thirties, on the same sheet.
|
||
|
||
**Induction never demotes you.** A former police officer already trains Firearm
|
||
(Pistol) and Tradecraft, so induction skips them rather than re-teaching them at a
|
||
worse number — the generated Law Enforcement agent carries pistol 58 and tradecraft 58
|
||
from the trade, not 26 from the course. They do end up with two sidearms, their own
|
||
revolver and the issued pistol, which reads as correct rather than as a bug.
|
||
|
||
**check-kits now guards trades and induction too.** Same standard as a posting: every
|
||
kit key must name a real item, every skill and talent must resolve, no posting may be
|
||
issued a weapon it cannot use, and INDUCTION is checked once because a typo in it is a
|
||
typo in ten characters at once. Both new paths were verified by breaking them:
|
||
|
||
criminal (Criminal): issued Marksman rifle, trains no firearm:rifle
|
||
induction: issues Compact pistol, teaches no firearm:pistol
|
||
|
||
## Rolling them yourself
|
||
|
||
The generator can produce eight characteristics in a millisecond and nobody at a table
|
||
has ever wanted that. There is now a tickbox — *Roll the characteristics myself* — that
|
||
walks them one at a time, each roll announced to chat so the dice actually fall, with
|
||
"Take them" locked until all eight are down and a rolled characteristic standing once
|
||
it is rolled.
|
||
|
||
**A hand-rolled character is taken exactly as it fell.** The automatic path takes the
|
||
best of three for the posting's key stat and applies the species shift; the hand-rolled
|
||
path does neither, and the code says why. Verified: eight nines passed in came back as
|
||
eight nines for both a baseline and a vesh, while the automatic path on the same
|
||
posting produced STR 17.
|
||
|
||
Worth recording as a probe error rather than a defect: my first check compared the two
|
||
sets with `JSON.stringify` and reported a mismatch. Every value was identical — the key
|
||
order was not. The rule was right and the comparison was wrong.
|
||
|
||
## PAPER MARGIN — a doctor, eleven months in
|
||
|
||
A farm at the end of a private road in Angus. A person is in two places and both are
|
||
attending work; the town has explained it as twins for eleven days. The team: **Dr
|
||
Anwen Pryce**, probationary, recruited from A&E — and two career officers, a
|
||
Containment Officer and a Pursuit Officer.
|
||
|
||
The generator handed this case its own best argument for trades. The truth page reads
|
||
*there is no original left; both are copies, and one of them knows*, and it closes by
|
||
one of them being taken into custody. **Telling two identical people apart is a medical
|
||
question**, and on that team Medicine reads 42, 1 and 1.
|
||
|
||
**The two halves showed up immediately, in both directions.**
|
||
|
||
*The doctor was the only one who knew what she was looking at.* Anomaly Lore: Pryce 37,
|
||
Kobe 0, Halloran 0. Tradecraft: 31, 5, 5. The career officers train neither, so the
|
||
newest person on the team was the only one carrying the department's own vocabulary —
|
||
which is induction working exactly as intended and reads as true at a table.
|
||
|
||
*And she was hopeless at the part that closes cases.* Her Persuade is **23**. Offered
|
||
the duplicate the truth, rolled 95, and it read her on 37 against Insight 60. She could
|
||
crack the case and could not talk anyone through it.
|
||
|
||
**The scene the whole design was for.** The physical examination failed by three — of
|
||
course it did, they are identical — and the bloods came back **critical on 02**. Both
|
||
samples are the same sample. Not twins, not an original and a copy: the same person,
|
||
twice. Neither career officer could have found it and neither would have thought to look.
|
||
|
||
**R-226 fired, correctly and visibly.** Pryce trained Tradecraft 31; Halloran trained
|
||
Navigate 34. The phase lead went to Navigate — *Tradecraft first, but not when it is
|
||
plainly worse* — and Halloran led all four phases, all four succeeding, with Containment
|
||
producing a special on 06.
|
||
|
||
**And the shield issued two commits ago turned up unprompted.** Kobe is Containment, so
|
||
she now draws a riot shield; it was slung, she raised it, and went from 7 everywhere to
|
||
10 on torso and arms. The duplicate never touched her. It also never touched anybody —
|
||
three shots, 77, 57, 95 — while Kobe's rifle put 5 through its chest and then 12 into
|
||
its **bare right leg**, because armour coverage applies to the opposition too.
|
||
|
||
The costs were paid as written: *meeting it costs 2 Coherence, killing it costs 3.*
|
||
Everyone dropped to 8; Kobe, who shot her own duplicate, ended **Loose at 5** and −5%
|
||
to Communication.
|
||
|
||
The parish had wanted a body back for years and now there was one that would bury.
|
||
Making it into something a family could be given is a doctor's job, and she rolled
|
||
**42 against 42** — a success by nothing at all.
|
||
|
||
### One probe error, worth recording
|
||
|
||
`endOfCase` returned an empty array and I nearly wrote it up as a regression of R-210.
|
||
It was not. `skillRoll` ticks experience only when told *which skill* was used, and my
|
||
ad-hoc rolls passed a rating and a category but no `skillFamilyId` — the same omission
|
||
R-222 found in `resolveLegDialog`, reproduced by hand in a test harness. Re-rolled with
|
||
the skill named, the tick appeared and the improvement roll landed: **Medicine 56 vs
|
||
42% → +6 → 48%.** The rule was right; the probe was not naming its skill.
|
||
|
||
## PAPER HARBOUR — the criminal
|
||
|
||
A pier that closed in 1974. A Custodian team crossed nineteen hours ago on a routine
|
||
survey and their beacon is still transmitting; three of the four are still alive and
|
||
the fourth is what the beacon is. The team: **Sennen Roake**, probationary, recruited
|
||
out of a city with a caution the department has quietly lost — plus a Field Lead and a
|
||
Technical Officer.
|
||
|
||
The generator's gap, unprompted: *one agent has a connection to this place that the
|
||
assigning officer either missed or did not miss, and it surfaces when somebody uses
|
||
that agent's name before it is given.* Phase 2 forced that encounter.
|
||
|
||
**He held it.** Fast Talk 14 against 48 — a clean success — and Sowande missed the read
|
||
on 42 against Insight 32. Nobody on his team knows. The case's own pressure, produced by
|
||
the tables and resolved by the one skill a criminal has instead of composure.
|
||
|
||
**The phases went badly and the ramp is why.** 52%, then −5%, then Difficult at 26%,
|
||
then −15%. One success and three failures, Containment 10 → 7, and the window clock at
|
||
1. That is the retuned ramp doing exactly what it was retuned to do, on a team whose
|
||
best phase-lead skill was a Field Lead's Tradecraft 52.
|
||
|
||
**Then the case turned on a warrant card.** The keepers of the ground — twelve of them,
|
||
an arrangement none will describe — want *"to be relieved. Somebody, anybody, official
|
||
enough to take the duty on."* Every agent on the team is carrying exactly that, and the
|
||
probationer with the quietly-lost caution is the one who handed it over: Bargain 27,
|
||
favourable, rolled 33. **Impression −1 → +1.** He insulted them in their own language
|
||
and then gave them the first honest thing anyone had offered them in four generations.
|
||
|
||
And the one moment that was unambiguously his: the first team locked the log in behind
|
||
them, and he opened it on **11 against Sleight of Hand 50** with lock rakes and a master
|
||
key ring. The improvement rolls afterwards landed on Fast Talk and Sleight of Hand —
|
||
the only two things he actually did.
|
||
|
||
### R-246. A fumbled greeting cost nothing
|
||
|
||
`contactOutcomes` has said `fumble: { signal: -1 }` since the social pillar was built,
|
||
and impression was adjusted **only inside the `offer` branch**. So the −1 was computed
|
||
on every botched greeting and thrown away: a team could insult a group in their own
|
||
language all afternoon at no cost, and only a formal offer could ever move how they
|
||
were seen. Found by Roake fumbling a signal on 99 and the standing not moving.
|
||
|
||
Impression now falls on a fumble at any stage and still only rises on an offer. The
|
||
narrower fix matters: `read`'s delta counts **facts learned**, not standing, so "apply
|
||
the delta always" would have paid the team impression for doing research. Verified by
|
||
rolling until a fumble landed — 99, delta −1, impression 0 → −1, standing −1.
|
||
|
||
### Two notes on the trade, and one on my own play
|
||
|
||
**A probationary criminal is out-sneaked by a career Field Lead** — stealth 47 against
|
||
60 — because trade skills are rated on the agency tier bands. It is defensible (a
|
||
probationer is a probationer, and at officer tier the criminal would sit at 45–60) but
|
||
it is worth deciding deliberately: a burglar recruited last month has been picking locks
|
||
for fifteen years, and the argument for giving trades a tier-independent band for their
|
||
*core* is the same argument that says a newly recruited doctor is already a good doctor.
|
||
Left as it is; flagged rather than changed.
|
||
|
||
**I invented a step.** After the extraction phase resolved — *"you are out, but not
|
||
cleanly"* — I rolled Transposition for all three to get them home, and all three failed
|
||
on ratings of 1, 1 and 6. That was not a step the case had; the phase had already put
|
||
them out. Recorded because the rolls happened and the log should say they did not mean
|
||
anything.
|
||
|
||
## Consolidation pass, after a Codex review
|
||
|
||
Codex reviewed the last week's work and recommended stopping to consolidate before
|
||
adding more mechanics. It was right, and four of its findings were real defects rather
|
||
than style notes. I verified each against the source before acting on any of them.
|
||
|
||
**Trade expertise was coupled to agency rank (Codex #1).** This is the thing I flagged
|
||
myself after the PAPER HARBOUR play-through and then left alone, which was the wrong
|
||
call — flagging a defect is not the same as fixing it. Trades used the tier bands, so a
|
||
probationary burglar came out at 35–50% and a veteran one at 65–80%, as though fifteen
|
||
years of picking locks were something the department conferred. `TRADE_BANDS` is now
|
||
fixed at 50–65 core and 30–50 support at every rank, and **induction now scales with
|
||
service instead** — 20–32 probationary up to 45–60 veteran, because that half genuinely
|
||
is the department's. The two axes were the wrong way round. Measured after: a criminal's
|
||
stealth 60 at probationary and 69 at veteran, the difference now coming from bonus
|
||
points rather than the band; induction tradecraft 27 → 55.
|
||
|
||
**Bonus points were sprayed across every skill in the game (Codex #2).**
|
||
`grantFullSkillList` runs immediately before the bonus-point loop and puts all
|
||
fifty-nine skills on the sheet, and the loop picked from `actor.items` — so a veteran's
|
||
ninety points landed anywhere, including skills the posting has nothing to do with. The
|
||
comment two hundred lines above has always said "spread over the core skills". It never
|
||
did. Now confined to a `trainedKeys` set built from the posting or trade plus induction.
|
||
Verified on a veteran Containment officer: ten skills raised, all ten trained.
|
||
|
||
**"Random species" was not random (Codex #3).** The dialog offers *Random (rolled
|
||
separately for each)* and then passed `"baseline"` into both generators, which support
|
||
random perfectly well. Twelve rolls now produce baseline ×8, vesh ×2, cadence ×2.
|
||
|
||
**No behavioural tests (Codex #4).** The strongest point in the review, and the one I
|
||
would not have raised myself. The three guards are static — they prove the tables agree
|
||
and that no rule is written twice — and **not one of them can tell whether a rule is
|
||
read**, which is the only kind of defect this project has ever shipped. R-246 was
|
||
confirmed by rolling a hundred and twenty times waiting for a 99. That is not evidence,
|
||
it is a hope.
|
||
|
||
`tools/check-behaviour.mjs` now runs 28 deterministic tests, gated into the build
|
||
alongside the other three, covering R-231, R-232, R-243, R-245, R-246, graded defences,
|
||
the lamp rule and the shape of every posting and trade. Proved it bites by re-breaking
|
||
R-246 and watching the build refuse.
|
||
|
||
**The register moved out of the engine.** `ROLES`, `TRADES`, `INDUCTION`, `TIERS` and
|
||
the issue lists are now `postings.mjs`, a Foundry-neutral module the engine, the
|
||
rulebook generator, the guards and the tests all import normally. They were previously
|
||
pulled out of `ringbrp.mjs` with a regular expression and `eval` — which worked, and was
|
||
a trap: reformat a table and every check depending on it stops checking without saying
|
||
so.
|
||
|
||
**And the one that only a browser could catch.** My first version of that extraction
|
||
used `export { … } from "./postings.mjs"`, which re-exports without creating local
|
||
bindings — so `ROLES` was undefined inside the engine and the system threw on `init`.
|
||
`node --check` passes that happily. Loading it in Foundry does not.
|
||
|
||
Smaller items, all as Codex described them: the contact doc comment still said "only
|
||
OFFER moves standing" after R-246 changed it; `check-kits` accepted `mas` and `app` as
|
||
characteristics, which are the *display names* of `siz` and `cha`, so a posting
|
||
declaring one would have passed the guard and produced an undefined characteristic —
|
||
latent only because nobody had tried it, and now derived from `CHARACTERISTIC_DICE`
|
||
rather than retyped; `package.json` said 0.7.0 against the manifest's 0.8.0; and the
|
||
README's counts were three revisions stale, so they are now generated by
|
||
`tools/update-readme.mjs` at build time between markers.
|
||
|
||
**Deferred, with reasons.** Splitting `ringbrp.mjs` — 6,700 lines — into generation,
|
||
contacts, combat and UI modules is right and I have not done it. It is a multi-session
|
||
refactor whose whole risk is silent breakage of exactly the kind the extraction above
|
||
just demonstrated, and it wants doing when there is nothing else in flight, not at the
|
||
end of a session that has already moved four subsystems.
|
||
|
||
**On the method.** Codex's closing point is the correct one and I am adopting it: the
|
||
play-throughs find integration failures that static review misses, but the narrative log
|
||
should sit *above* executable evidence rather than instead of it. Every numbered defect
|
||
that can be reproduced without a browser now has a test. Those that cannot — anything
|
||
inside the generators, the sheets or the contact engine — are named in the test file's
|
||
header as out of its reach, so the suite does not look more complete than it is.
|
||
|
||
## PAPER WINDOW — the starter scenario, as a Foundry Adventure
|
||
|
||
Codex's six-player introductory case is now in the system as an importable scenario
|
||
module. **Adventure**, not a journal: a scenario that arrives as nine loose documents a
|
||
GM has to assemble is a scenario that gets read rather than run.
|
||
|
||
`ringbrp.starter` contains one Adventure holding:
|
||
|
||
* the **GM scenario** in twelve pages, in running order;
|
||
* the **seven player handouts** as their own journal at ownership OBSERVER, so they can
|
||
be revealed one at a time without the GM changing permissions on seven pages before
|
||
the session — the GM journal stays hidden;
|
||
* the **six pregens**, built through the same `buildActor` path the shipped pregens
|
||
use, so they arrive with full skill lists, kit, hit points and linked tokens;
|
||
* the **cast** — Evelyn Vale, both Lorna Vales, Gareth Moss and the Cancellation Man —
|
||
as statblocks a GM can drag onto a scene;
|
||
* the **Borrowed Authority** as a real credential item with scope, Trust 2 and its three
|
||
red flags, rather than a paragraph in the brief;
|
||
* the **case file** already at Containment 8, Resources 8, Clearance 6 with its four
|
||
phases loaded at the scenario's stated difficulties and modifiers.
|
||
|
||
**The pregens are fixed, not rolled.** Ratings sit at the middle of each band rather
|
||
than being generated, because a pregen that varies between installs is not a pregen and
|
||
six tables running the starter should see the same six agents. Verified after a live
|
||
import: Nia carries the team's highest Tradecraft at 53 as the text requires, Callum's
|
||
riot shield is **slung** so raising it is still the player's decision, and the three
|
||
probationers show the trade/induction split the scenario is built to teach — Elin
|
||
Medicine 58 against an induction of 26, Owen Electronics 58, Sennen Sleight of Hand 58.
|
||
|
||
The expansion from a posting or trade into a full sheet reuses the register and the
|
||
induction rule rather than restating either, so the pregens cannot drift from the
|
||
generator: change `TRADE_BANDS` and the starter's doctor changes with it.
|
||
|
||
**Verified by importing it rather than by building it.** The pack builds, the Adventure
|
||
is well formed, the import completes with no errors, the case roster resolves through
|
||
stable keys after import (raw ids do not survive one), the handouts come in
|
||
player-visible and the GM text does not, and **phase one runs straight out of the box** —
|
||
Nia leading the cordon on Tradecraft 53. Then everything imported was deleted again,
|
||
which the pack does not notice.
|
||
|
||
## PAPER WINDOW — played with the shipped pregens
|
||
|
||
Ran the starter end to end with the six pregens exactly as they import. It closed, and
|
||
it found four defects in what I had shipped an hour earlier — all four in the scenario
|
||
data rather than the engine, and every one of them silent.
|
||
|
||
**R-247. The Borrowed Authority did not work, and said nothing.** I wrote the
|
||
credential's scope as `"operate"`, which is not one of the five scope tiers. An
|
||
unrecognised scope ranks 0 — *nothing* — so `scopeStanceFor` returned "impossible" for
|
||
a routine site inspection by a surveyor at a demolition site. No error, no warning: the
|
||
credential was simply inert, and the first scene of the starter is the Borrowed
|
||
Authority tutorial. Fixed to `"routine"`, guarded in check-kits, and covered by a test
|
||
that asserts an unrecognised scope makes everything impossible so the failure mode is
|
||
documented rather than rediscovered.
|
||
|
||
**R-248. The contact subsystem was unreachable — in the scenario built around it.** I
|
||
gave the case file a standing record for the Returned Clerks and no group *item*.
|
||
`resolveContact` looks up the group and returns null without one, so READ, SIGNAL and
|
||
OFFER did nothing at all. PAPER WINDOW's central scene is a three-stage negotiation with
|
||
those clerks. The group is now data in `scenario-starter.mjs`, consumed by the builder,
|
||
and a test asserts every standing on a shipped case file has a group behind it.
|
||
|
||
**R-249. A phase kind written to the wrong field.** I wrote `kindId` on the legs;
|
||
`resolveLegDialog` reads `leg.kind` and falls back to a regular expression on the terrain
|
||
prose. It worked only because "Extraction — closing the register" happens to begin with
|
||
the word. Rename a phase and the return clock stops moving.
|
||
|
||
**And one that was not a defect.** Phase four resolved as Difficult when the scenario
|
||
table says Average — because Resources had reached 0 and destitution makes every
|
||
remaining phase Difficult. That is the rule working, and it was the best beat in the
|
||
session: they ran out of supplies and the way home doubled in difficulty.
|
||
|
||
### How it actually played
|
||
|
||
The credential carried them in and then the stretch for an asbestos exclusion spent it
|
||
to **Trust 0** — two hours bought, and nothing left for later. Phases one, two, three and
|
||
four **all failed**, which the scenario is explicitly built to survive: Containment 8 → 2,
|
||
Resources 8 → 0, Clearance 6 → 5, and the case still closed.
|
||
|
||
The investigation split cleanly across four players and three of the four discoveries
|
||
landed. Amira's READ turned up **both** taboos on one roll — Read the Room granting the
|
||
extra fact — and Elin **criticalled on 01** for the desire. SIGNAL and OFFER at
|
||
favourable took the clerks to **Standing 2**, which fired Evelyn's rewards automatically
|
||
and put the clean settlement on the table.
|
||
|
||
The Cancellation Man opened with a **critical on 01** against Callum, whose riot shield
|
||
was raised and whose arm therefore had 10 points of armour. A critical ignores armour
|
||
entirely: 10 through, Major Wound, 15 → 5. That is the shield lesson and the graded
|
||
defence lesson in a single roll, in round one, without any prompting from me.
|
||
|
||
Then the closing scene did the thing the scenario promises: **two of six requirements
|
||
succeeded and the settlement still completed.** Nia's authority failed and spent the
|
||
Clearance anyway; Callum's containment failed for a point; Elin certified them and is
|
||
now personally responsible for reviewing all 317 cases; Owen's repair worked and took
|
||
the last of the spare cells. Sennen's **special on 07** recovered the crown stamp with
|
||
the procedure intact, using the +20% Evelyn had given him at Standing 2 — a reward
|
||
granted by the contact engine, spent in the final scene. That chain is the whole design
|
||
working unassisted.
|
||
|
||
### One thing the starter needs saying out loud
|
||
|
||
**Nobody on this team can aim the return.** Transposition is trained by exactly one
|
||
posting in the register — the Anchor Officer — and none of the six has it, so all six
|
||
roll the printed 1%. The rule that failure gets an agent home *near* the target and
|
||
never strands them is what makes that survivable, but asking a GM to roll six dice that
|
||
are all going to fail teaches nothing. The extraction page now says so: narrate it, roll
|
||
only if somebody wants a clean arrival, and note at debrief that the department sent six
|
||
people to a crossing and no anchor. That is the campaign rather than an oversight — but
|
||
it is a deliberate choice a GM should be able to see, not a hole they fall into.
|
||
|
||
## BLACK PLATFORM replaces PAPER WINDOW as the starter
|
||
|
||
The starter is now an action case: a train running through a station whose rails were
|
||
taken out in 1976, two engineers aboard, and eleven minutes until it comes back. Go
|
||
down, take the platform, board, rescue, fight forward along a moving train, stop it.
|
||
|
||
It is a better first case than PAPER WINDOW for the same reason it is simpler. PAPER
|
||
WINDOW's centrepiece was a three-stage negotiation and a six-signature settlement —
|
||
excellent, and a lot to hold in your head in hour one. BLACK PLATFORM is a straight
|
||
line with one clue scene and no social subsystem, which also means it cannot reproduce
|
||
R-248: **no groups, no standings, nothing to leave unwired.**
|
||
|
||
**Every agent can fight, deliberately.** The scenario sets floors — a firearm at 42% or
|
||
better, a melee weapon at 40%, Brawl 40%, Dodge 40% — so that nobody is a spectator when
|
||
initiative is rolled on their first evening. The floors only ever RAISE a rating, so a
|
||
posting already better at something stays better: Callum's shield comes out at 53 rather
|
||
than the floor's 45, and Owen is still far better at engineering than at fighting.
|
||
Verified on import, all six.
|
||
|
||
The build now handles three things it could not before: **combat floors**, **extra
|
||
weapons** issued for a case, and a scenario with **no Borrowed Authority at all** — the
|
||
station is already shut to the public, so there is nobody to be somebody else in front
|
||
of. That last one had been assumed to exist and would have thrown.
|
||
|
||
Two new tests: every floor must name a real skill (a floor on a skill the catalogue does
|
||
not have would put a number on the sheet that no rule could ever reach), and every
|
||
starter agent must actually clear the fighting floors the handout promises them.
|
||
|
||
## The dialogs now ask questions
|
||
|
||
Sixty-two strings reworded. The dialogs were naming mechanics at people:
|
||
|
||
| Before | After |
|
||
|---|---|
|
||
| Situational modifier % | Anything else helping or hurting? (%) |
|
||
| Target cover | Is the target behind anything? |
|
||
| Difficulty | How hard is it? |
|
||
| Fire | How are you firing? |
|
||
| Range | How far away? |
|
||
| Light | How much can you see? |
|
||
| Applies itself | Working for you already |
|
||
| Difficult (÷2) | Difficult — half your skill |
|
||
| Beyond twice the long band. The shot is impossible. | Too far away to hit. Get closer or use something with more reach. |
|
||
| That is Impossible. No roll is permitted. | That cannot be done at any skill. Try something else. |
|
||
|
||
The principle: **a label should ask the question, and a hint should say what it does to
|
||
the number.** The difficulty ladder now says what it does — "Easy — double your skill",
|
||
"Difficult — half your skill" — rather than printing an operator. Notifications say what
|
||
to do next instead of what went wrong.
|
||
|
||
The generator dialog got the same treatment: "Recruited as" is now "Where do they come
|
||
from?", "Tier" is "How senior are they?", and the hand-roll option explains what it
|
||
actually costs you — "You keep exactly what you roll — no adjusting the best one, no
|
||
species bonus."
|
||
|
||
Checked afterwards that every localisation key still referenced by the engine or a
|
||
template exists; the six apparent misses are Handlebars concatenation prefixes rather
|
||
than lookups.
|
||
|
||
## The player primer, and the journals finally look like the game
|
||
|
||
Ten pages a player reads before session one, in their own compendium at OBSERVER
|
||
ownership: nine of setting and one cheat sheet. Plus the branding that all four
|
||
journals in the system had been going without.
|
||
|
||
**The voice was the first thing to fix.** The draft called the organisation "the
|
||
Bureau" throughout. Everything else in the game — twelve rules pages, the starter, 927
|
||
localisation strings — calls it *the department* or *the agency*. One name, and it is
|
||
not Bureau.
|
||
|
||
**Where the draft was thin, it got filled rather than padded.** The character section
|
||
now carries the seven principles as an ordered list a player can actually work from;
|
||
"what it costs" gets the real Coherence band table rather than a paragraph describing
|
||
one, and says what the anchor in the kit is for; and the recruitment page separates
|
||
*what continuity means in practice* from *what it is not*, because "it is not a
|
||
superpower" is the single most important sentence for a new player and it was buried.
|
||
|
||
**The cheat sheet's numbers are computed, not typed.** Hit points, Major Wound, damage
|
||
modifier, the defence penalties, the range ladder and the Coherence bands all come out
|
||
of `rules.mjs` and `lang/en.json` at build time. A hand-typed cheat sheet would have
|
||
been the sixth time this project shipped a number that disagreed with the code — and it
|
||
would have been the one the players were holding. It closes on six questions new
|
||
players actually ask, including "am I going to die?" answered honestly.
|
||
|
||
### R-250. The design tokens were never on `:root`
|
||
|
||
The journals rendered as Foundry's default parchment, and chasing why took four wrong
|
||
guesses that are worth recording, because each one looked right.
|
||
|
||
1. **The hook name.** v14's journal sheet is an ApplicationV2: the hook is
|
||
`renderJournalEntrySheet`, not `renderJournalSheet`.
|
||
2. **`element` is the node.** On AppV2 it is the root element, not a jQuery wrapper.
|
||
Reaching for `[0]` silently returns the first *child* — which is how the first
|
||
version appeared to work, put the class on a header button, and changed nothing.
|
||
3. **The page container.** There is no `.journal-page-content` in v14. The prose sits
|
||
directly inside `article.journal-entry-page`.
|
||
4. **And then the real one.** With the class on the right element and the selector
|
||
matching, `font-size`, `line-height` and `padding` from my rule were all applying —
|
||
and every colour was not. `--rb-deep` resolved to nothing. The palette is declared
|
||
on `.ringbrp-sheet, .ringbrp-chat, .ringbrp-window` and **never on `:root`**, so a
|
||
journal carrying a new class inherited no tokens at all. Every `var()` silently
|
||
became nothing, which CSS treats as "unset" rather than as an error.
|
||
|
||
The tell was that the *numeric* properties from the same rule worked while the coloured
|
||
ones did not. A rule that half-applies is not a specificity problem, and I spent three
|
||
attempts treating it as one — including bumping the system version on a cache theory
|
||
that was wrong.
|
||
|
||
Adding `.ringbrp-journal` to the token block fixed all of it at once.
|
||
|
||
**Now branded:** the primer, the generated rulebook, the starter's GM text and its
|
||
handouts. Scoped by a flag, so another module's journals in the same world are
|
||
untouched. The contents rail is the yellow-on-navy of the sheet tabs, read-aloud gets a
|
||
spot-colour margin rule, diagrams are monospaced in a bordered block, and the cheat
|
||
sheet is a grid of cards so it can be skimmed under pressure rather than read.
|
||
|
||
Version bumped to 0.9.0 — which, incidentally, is what makes Foundry re-fetch the
|
||
stylesheet for anyone updating.
|
||
|
||
### 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.
|
||
- `exposedHideFor` now takes the location's kind and returns natural armour **unhalved for
|
||
`vital`**. 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 down
|
||
- `damageAfterArmour` — 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.
|
||
|
||
## R-261 — the focus-fire claim is now an artifact, not a sentence
|
||
|
||
R-258 put a tactic on a GM-facing page from an unguarded harness. R-259 found it worth
|
||
nothing. R-260 found the replacement right for the wrong reason. Three corrections, each
|
||
one landing as prose with nothing checking it — the arrangement that let ddc4f99 describe
|
||
a game nobody was playing.
|
||
|
||
`tools/focus-baseline.json` now records, per creature, the pack size, the spread-fire and
|
||
focus-fire win rates and the gain. `check-focus` re-measures all of it on every build and
|
||
compares **exactly**, with the frozen party, the seeds, the run count and the seeding
|
||
scheme recorded alongside so numbers taken under different conditions are refused rather
|
||
than compared. The bestiary **reads** the artifact instead of restating it, and
|
||
check-bestiary refuses a page that has fallen behind. Page, guard and simulator now cannot
|
||
disagree; the twelfth guard is the only thing entitled to state the number.
|
||
|
||
**The pack size is recorded, not re-chosen.** A fight at 0% or 100% cannot show an effect,
|
||
so `--update` picks the size nearest an even fight and writes it down. If the guard chose
|
||
again each run, a creature that had changed would quietly move to a different question and
|
||
pass. 31 of 47 creatures land in the measurable band; the other 16 are recorded as pinned,
|
||
with the rate that pinned them.
|
||
|
||
**It also guards the claim, not just the numbers.** The page says focus fire helps in every
|
||
fight that is in doubt. `check-focus` fails if that stops being true — if any row's gain
|
||
goes to zero, or stops clearing its own seed-to-seed noise. That is a different failure
|
||
from drift: drift means re-record, this means rewrite the page.
|
||
|
||
**The run count was chosen by evidence.** 1000 fights × 3 seeds costs 8.5s and takes the
|
||
whole suite from 2.5s to 13.7s, which is a real tax. I tried 500 to halve it and the
|
||
claim-check failed: at 500 runs one row's gain no longer clears its noise, so the page's
|
||
"without exception" is not supported by that much sampling. The build pays eight seconds
|
||
for a sentence it is entitled to print. Recording twice at 1000 produced byte-identical
|
||
files, which is the determinism the exact comparison depends on.
|
||
|
||
**Negative-tested four ways**, all firing with the right message: a creature quietly made
|
||
nimbler (the redcap from dodge 75 to 85 — spread 40.6% → 27.6%, caught), a baseline taken
|
||
under different seeds, a hand-edited page, and the claim itself failing at 500 runs.
|
||
|
||
Measured, and now guarded: **8 points** of win rate against a pair, **14** against three or
|
||
more, best case 6 × The margin at 42.0% → 61.1%.
|
||
|
||
*Note: guard eleven, `check-rollable`, arrived from another session mid-build; this is
|
||
twelve. Both were added to `package.json` and the README list without either clobbering
|
||
the other, and the README's count word tracked it on its own.*
|
||
|
||
## R-262 — a real skill is not a route, and the first guard for that reported success
|
||
|
||
`check-scenarios` resolves every skill a scenario NAMES against the catalogue. That is a
|
||
different question from whether anybody at the table can roll one, and the gap is wide
|
||
enough to hide a clue in.
|
||
|
||
CLEAN GROUND shipped four commits with three clue routes gated on **Track** (base 10),
|
||
**Navigate** (base 10) and **Science (Botany)** — base **1**, so the 1% floor was the whole
|
||
of it. Every guard passed at every commit, because all three are perfectly real skills that
|
||
resolve perfectly well. Nothing in the build had an opinion about who was holding the dice.
|
||
|
||
A desk playtest found it, by auditing the acts against the sheets the scenario actually
|
||
casts, before rolling anything. That audit is mechanical, so it is now `check-rollable`.
|
||
|
||
**Two tiers.** Corpus-wide, some duty-roster agent must reach **VIABLE** for every skill any
|
||
scenario names — a scenario may bring anyone, so a skill no roster agent can roll is a dead
|
||
route whoever turns up. Per scenario, a document that **declares** its cast is held to that
|
||
cast instead.
|
||
|
||
**VIABLE is 25, and it is derived rather than chosen.** It is the commonest base chance in
|
||
the catalogue — what an untrained agent brings to Spot, Listen or Brawl — so it is the level
|
||
the game itself treats as worth attempting. Below it a clue is not gated, it is buried: at
|
||
10 the GM improvises a bridge four times in five, and at 1 the tag is decoration.
|
||
|
||
**The first version of this guard passed the bug it was written for.** It inferred the cast
|
||
by scraping `pc_` keys out of the prose, which swept up the substitutes named in CLEAN
|
||
GROUND's player-count scaling — a declared six became an inferred nine, Sandoval and his
|
||
Track 35 came into scope, and the strict tier went green. The cast is now declared in a
|
||
`<!-- cast: ... -->` comment and read off the raw text, because `scenarioText` strips HTML
|
||
comments. A guard that guesses its own scope is worse than no guard, because it reports
|
||
success and stops anyone looking.
|
||
|
||
**Verified load-bearing against the real document, not a fixture.** Re-injecting the
|
||
original Track tag into `CLEAN_GROUND.md` fails the guard and names the skill, the base
|
||
chance and the declared cast. Worth recording *why the fixture was not enough*: the
|
||
corpus-wide tier would never have caught this at all, because **Lindqvist trains Science
|
||
(Botany) at 40**. The skill is rollable by the roster and simply not by the six who were
|
||
cast, so the first fixture test passed and was reassuring and wrong. Only the declared-cast
|
||
tier finds this defect class.
|
||
|
||
`check-scenarios` now exports its tag parser behind the `invokedDirectly` pattern
|
||
`simulate.mjs` already uses, rather than letting a second copy grow in the new guard —
|
||
which is the one thing this repository forbids. `update-readme` caught the omission fairly
|
||
on the first attempt, since it cross-checks the advertised list against `npm run check`.
|
||
|
||
## R-263 — the simulator can be read now, not just totalled
|
||
|
||
Every guard in this suite reports a number per creature, and a number cannot say WHY a
|
||
fight went the way it did. That gap is why R-258, R-259 and R-260 happened: each was
|
||
written from a side harness built to watch a fight, each modelled something slightly
|
||
different from the game, and two of the three were wrong in a way no guard could catch
|
||
because no guard was involved.
|
||
|
||
`runFight` now takes an optional `say` sink. It reports what the simulator has already
|
||
decided — the attack roll and its target, a defence and the penalty it was made at, the
|
||
landing level after a dodge downgrades it, damage against armour before and after
|
||
`armourAgainst`, the location, major wounds, disablement and death. **It never touches
|
||
the generator**, so a narrated fight and a silent one are the same fight. check-lethality
|
||
and check-focus both still match their baselines exactly with the hook in place, which is
|
||
the proof rather than the claim.
|
||
|
||
`tools/playthrough.mjs` (`npm run play`) is its consumer, added in the same commit
|
||
deliberately: a hook with no reader is precisely the defect this project keeps finding in
|
||
its own rules, and adding one to the measurement pipeline with only a scratchpad file
|
||
calling it would have been me committing the thing I have spent the session removing. It
|
||
takes its pack size from `focus-baseline.json`, so the fight you read is the fight
|
||
check-focus measures, and creatures recorded as pinned are played solo where targeting
|
||
cannot mean anything.
|
||
|
||
**What it shows, immediately.** Three redcaps, seed 20260913, played twice from the same
|
||
seed. Spread fire: the party is wiped in 10 rounds, 4 down, 3 dead, and **all three
|
||
redcaps are still standing at the end** — thirty-nine hit points spread three ways so that
|
||
none of it ever finished anything. Focus fire: the same opening, diverging at one target
|
||
choice in round 1, and the party wins in 20 with two on their feet. That is the +13.2
|
||
points check-focus records for this creature, seen once instead of averaged.
|
||
|
||
The defence ladder is legible in the log too — `Ben → Redcap A: 26 SUCCESS — dodged
|
||
(10 vs 45, -30 for defences already spent)` — a 75% dodge meeting the second attack of the
|
||
round at 45.
|
||
|
||
## R-264 — the harness was fighting a different animal
|
||
|
||
Reading a narrated supporter fight (R-263, the first time any fight in this project could
|
||
be read rather than totalled) showed it being hit on `armR` and `head`. It has wings. It
|
||
has never had arms. Three defects, all in the pipeline every published number comes from.
|
||
|
||
**1. It located hits by species, not body plan.** `simulate.mjs` read `defender.species`
|
||
where the game writes `spec.bodyPlan ?? spec.species ?? "baseline"` into `speciesProfile`
|
||
(build-packs.mjs:294). Four creatures were fought human-shaped: the barghest, the kelpie
|
||
and the church grim on two legs with arms, and **the supporter with no wings at all**, so
|
||
the fight its own tactics call the one the agents can win could not occur in a measured
|
||
fight.
|
||
|
||
**2. Armour was one scalar for the whole creature.** `worn + naturalArmour`, applied
|
||
everywhere, and the harness's locations had no armour field at all. So bare wings, the
|
||
grounded halving and the vital exemption — R-254 and R-255, most of a session's design —
|
||
were invisible to every number. Worn armour was summed the same way, which put a stab
|
||
vest on a cleaner's head.
|
||
|
||
**3. Nothing was ever grounded.** A wing could be ruined and the creature kept flying,
|
||
because the harness never changed a body mid-fight.
|
||
|
||
**The fix, in the game's own order.** Locate first, then apply what that location carries:
|
||
`locationArmourFor(slot.armour + naturalArmourOn(slot, exposedHideFor(hide, grounded,
|
||
kind)), worn, kind)`, every term imported. A ruined wing now calls `groundedPlanFor`, and
|
||
the wounds carry across by severity through `remapLocationDamage` — the same function the
|
||
actor's `_preUpdate` uses.
|
||
|
||
**A bug of mine that no guard would have caught.** `remapLocationDamage` returns
|
||
`{ damage, moved, rescaled }` and my first draft passed the whole object where a damage
|
||
map was expected, so **every wound a creature was carrying was forgiven the moment it came
|
||
down**. check-lethality would have passed it happily: fewer wounds means a longer fight,
|
||
which reads as a number moving, and the whole point of this commit is that numbers move.
|
||
Found by probing a landing by hand, not by the suite. The engine destructures it the same
|
||
way at ringbrp.mjs:462, which is where I should have looked first.
|
||
|
||
**What it cost. 20 of 47 creatures moved, 18 deadlier and 2 less.**
|
||
|
||
| | wiped | down of 4 |
|
||
|---|---|---|
|
||
| **The supporter** | 69.3% → **25.1%** | 3.38 → 2.16 |
|
||
| The stanchion | 31.9% → 37.4% | 2.29 → 2.52 |
|
||
| Nuckelavee | 16% → 20.3% | 1.70 → 1.98 |
|
||
| Long walker | 64.1% → 68% | 3.18 → 3.32 |
|
||
|
||
The supporter falls from second deadliest in the book to fourth, and that is the correct
|
||
direction: it was being measured as a 30-hit-point creature in uniform armour 9 that could
|
||
not be grounded, and it is actually a creature you can put on the floor, after which most
|
||
of it is armour 4. The small rises everywhere else are the party's own armour no longer
|
||
covering locations it never protected — a torso vest stops covering a head.
|
||
|
||
Focus fire was re-recorded too: 30 of 47 measurable now rather than 31, worth 8 points
|
||
against a pair and 12 against three or more, best case 3 × Spriggan at 38.9% → 55.5%.
|
||
|
||
**And check-focus caught the page overclaiming, which is what it is for.** Its claim-check
|
||
failed on the new numbers: focus fire still helps in all 30, but only 28 clear their own
|
||
noise where all 31 once did, and the page said "each time". The guard was asserting more
|
||
than the page needs to: the bestiary prints the above-noise COUNT from the artifact and so
|
||
cannot overstate it however it moves, and what the page actually asserts outright is that
|
||
concentrating fire helps. The guard now checks exactly that, and the page rewrote itself
|
||
to "in 28 of them" on regeneration.
|
||
|
||
## R-265 — two generators, so a citation named two different fights
|
||
|
||
`tools/playthrough.mjs` exists to show a reader WHY a measured number is what it is. I
|
||
cited it to another session — read seed 2, see the wipe the 45% row is made of — and the
|
||
reproduction failed in front of them. They reported different outcomes at both seeds and
|
||
guessed the cause correctly from the outside: the two tools were not drawing from the same
|
||
stream.
|
||
|
||
They were not. `simulate.mjs` used a private mulberry32 `makeRng`; `playthrough.mjs` had
|
||
its own linear congruential generator, written to look like it. Both deterministic, both
|
||
reproducible on their own, and **"seed 2" named a different fight in each** — which
|
||
quietly breaks the only thing the tool is for. Its own comment claimed "a seed here names
|
||
the same fight there". `check-focus` had a third copy of the same LCG, so the two lethality
|
||
guards described the same game with different dice.
|
||
|
||
`makeRng` is exported, and both files use it. A playthrough seed is now exactly the first
|
||
fight of `simulate.mjs --seed <n>`: verified by running `--runs 1 --seed 2` and the
|
||
playthrough side by side — 9 rounds, 4 of 4 down, 1 dead, both.
|
||
|
||
**The other half was my citation, not the code.** The command I sent omitted `--mode`, so
|
||
it plays BOTH targeting arms and prints two fights; they read the last line, I quoted the
|
||
first. The summary line now names the arm — `→ [random] PARTY WIPED …` — so a quotation
|
||
cannot be ambiguous.
|
||
|
||
**What it cost.** check-focus re-recorded under the shared stream: 31 of 47 measurable
|
||
again, and the numbers move by a point or two each. What it buys is that every figure in
|
||
the focus baseline is now drawn from the same dice as the lethality baseline, and the
|
||
bimodality analysis below reproduces the published means **exactly** — 2.31, 3.03, 0.40 and
|
||
0.42 of 4 or 6 down, against the four rows CLEAN GROUND publishes. Under the old LCG it
|
||
agreed to within a decimal, which looked like corroboration and was really two different
|
||
experiments landing near each other.
|
||
|
||
**The finding that started it survives, and is now checkable.** The four-player cut against
|
||
three of the column is bimodal: 23.1% of fights end with nobody down, 45.0% with everybody
|
||
down, and 11% land on the published mean of 2.31. Six players against six of the column is
|
||
not bimodal at all — 10 to 18% at every outcome — so this is a property of the cut rather
|
||
than of the creature. The mechanism is a race: put one of the three down inside five rounds
|
||
and the wipe rate falls from 58.5% to 19.7%; put down two and it is zero wipes in the 84
|
||
fights that get there. 68% of fights reach round five with none of them down.
|
||
|
||
**The lesson is the one this log keeps recording, one layer out.** A number that cannot be
|
||
reproduced is a claim, whoever holds it. It was caught because another session tried the
|
||
citation rather than trusting it, which is exactly what a citation is for.
|
||
|
||
## R-266 — the coin is the first disabling blow, and it lands in round two
|
||
|
||
R-265 left a finding standing: the four-player cut of CLEAN GROUND is bimodal — 23.1% of
|
||
fights end with nobody down, 45.0% with everybody down, and only 11% near the published
|
||
mean. It said the fight is *decided by round five*. That was true and it was the wrong
|
||
frame: round five is where the outcome is already legible, not where it is decided.
|
||
|
||
**Reading one fight from each end says why.** Seed 5 is the clean sweep. Neil puts 5 into
|
||
a leg on the first exchange; 5 of 10 is exactly the major-wound threshold, so one of the
|
||
three is disabled before anybody else has landed anything. A is down in round 2, C in
|
||
round 3 on a critical, B in round 4 before it acts. **The party is hit once in the whole
|
||
fight** — 4 points through Neil's armour. Seed 2 is the wipe, and its first two rounds
|
||
produce a single 3-point scratch; the column lands three blows in round 3, Kate goes 12 to
|
||
0 and Arun is disabled by a critical, and the cut is two effective attackers against three.
|
||
|
||
**Both sides disable on one good hit.** A LOCATION is disabled when the damage it has
|
||
taken reaches that location's own capacity — `locationMaxHp`, a fraction of total hit
|
||
points — which is 5 for a neighbour's leg or torso and 4 for its arms, against 6 and 5 for
|
||
an agent. (Not, as an earlier version of this entry said, at half of maximum hit points:
|
||
that is `majorWoundFor`, it feeds only how long a dying character lasts, and being taken
|
||
out of the fight is `conditionFor` at hp <= 2. See R-270 — the invented rule gave the right
|
||
answer for legs and torsos, which is why it survived being read.) So an
|
||
average blow takes a fighter out of the fight. Every disable costs its owner 30 points off
|
||
the relevant skills — `locationEffectsFor`, an arm or a leg — which for a 40% attacker is
|
||
three quarters of their hit chance, so it thins the return fire and makes the next disable
|
||
likelier. It does not remove them: a disabled agent keeps acting, keeps drawing attacks
|
||
and keeps being a target, which is why the compounding is a slope rather than a cliff. Nothing pulls a fight back
|
||
toward the middle once it has tipped, which is the whole of the bimodality.
|
||
|
||
**Measured, `tools/first-blood.mjs`, 2000 runs across three seeds:**
|
||
|
||
| first fighter out | share of fights | mean round | wipes among those |
|
||
|---|---|---|---|
|
||
| the party strikes first | 50.3% | 1.8 | **25.5%** |
|
||
| the column strikes first | 49.7% | 1.8 | **65.5%** |
|
||
|
||
A true coin, settled in round two, worth **40 points of wipe rate**. Six against six of the
|
||
column is the control: first blood still matters there and matters far less — 7.8% against
|
||
23.2%, a 15-point swing, at mean round 1.3. One fighter is a sixth of that line and a
|
||
quarter of the cut, so the same blow compounds less. This is a property of the cut, which
|
||
is what the histogram said by a different route.
|
||
|
||
*Figures above are the recorded artifact, not the scratch run this entry was first written
|
||
from. That run was one seed and gave 49.7/23.1/66.5 and a swing of 43. Averaging three
|
||
seeds moves it to 40, and the seed-to-seed spread of the swing is 2.1 — so the single-seed
|
||
figure was high by more than its own noise, which is the argument for recording it rather
|
||
than quoting it. See R-267.*
|
||
|
||
**Dominic should be read before anyone is handed him.** In both narrations he attacks at
|
||
`vs 1` — four attempts in the seed 5 fight, all of them hopeless. The four-player cut is
|
||
three effective attackers, not four, and a table that assigns Ashcroft to a player without
|
||
saying so has quietly made the coin worse.
|
||
|
||
**`tools/first-blood.mjs` was a reading tool and not a guard when this was written.**
|
||
That was the right place to stop and the wrong place to stay — see R-267, which is the
|
||
consequence of being told so.
|
||
|
||
## R-267 — the thirteenth guard, built because a peer refused to print the twelfth's number
|
||
|
||
R-266 measured why the four-player cut is bimodal and said plainly that the figure had no
|
||
guarded home, so it should not reach a page. The session holding the scenario agreed and
|
||
went further: it verified both configurations, said the frame was better than its own, and
|
||
**still would not publish it**, because the numbers already in CLEAN GROUND come out of the
|
||
guarded lineage and can be re-derived by a doubter while mine could not. It offered to
|
||
reframe the paragraph the moment a guarded home existed.
|
||
|
||
That is the correct call and it is the whole reason this entry exists. The refusal was
|
||
worth more than the finding.
|
||
|
||
**What it cost to fix: 0.7 seconds of build time.** That was the number I should have
|
||
checked before writing "a reading tool, not a guard" as though it were a considered
|
||
position. `check-focus` runs 1000 fights x 3 seeds x 47 creatures and takes 8.6s; this
|
||
runs 2000 x 3 x 2 configurations and takes 0.7s. There was never a cost argument.
|
||
|
||
**Shape.** `tools/first-blood.mjs` gained `--update` and `--check` and is now both the
|
||
reading tool and the measurement of record; `tools/check-firstblood.mjs` is a thin wrapper
|
||
that spawns it, the same arrangement as `check-bestiary`/`bestiary.mjs`. The reader and
|
||
the guard cannot disagree, because they are the same code path — which is the defect this
|
||
whole sequence started from, one layer down.
|
||
|
||
**What it asserts, and deliberately no more.** The recorded figures are compared exactly,
|
||
so any movement at all shows as drift. On top of that, two claims, both of which a page
|
||
would actually make: first blood in the cut lands within 5 points of even, and the swing
|
||
it is worth in the cut exceeds the swing in the six-a-side line by more than both
|
||
samplings' noise. A guard that asserted the swing's exact size would fail on news rather
|
||
than on error — R-261, which this suite has already learned once.
|
||
|
||
**Recording it changed the number, which is the argument in miniature.** The scratch run
|
||
said the first blow was worth 43 points. Three seeds say 40, against a seed-to-seed spread
|
||
of 2.1 — so the figure I sent another session was high by more than its own noise. It was
|
||
not wrong enough to mislead anybody and it was wrong enough to print.
|
||
|
||
**`tools/update-readme.mjs` could not count to thirteen.** Its `COUNT_WORD` table stopped
|
||
at twelve, which is exactly the hardcoded-list failure its own comment warns about two
|
||
lines above, committed by the file that exists to prevent it. Extended to sixteen, which
|
||
defers the same defect rather than curing it.
|
||
|
||
**Addendum — the coupling did not actually bite.** I told the scenario's session that this
|
||
guard converts a silent re-cast into a build failure. It believed me and accepted the
|
||
coupling on that basis. It was not true: the cast was written out by hand here, so a
|
||
re-cast would have left this measuring the old six and reporting success — the project's
|
||
own signature defect, committed inside the guard written to prevent a weaker version of
|
||
it, three hours after writing "a claim nothing checks" about something else.
|
||
|
||
It now reads the `<!-- cast: ... -->` marker check-rollable already established, derives
|
||
the cut by dropping the two the scaling note drops — by name, since "the first four" would
|
||
survive a reordering and quietly measure different people — and refuses to run at all if
|
||
the marker is missing. Proved by re-casting CLEAN GROUND in a throwaway worktree and
|
||
watching it fail: *"cut: the scenario now casts different people — no longer braithwaite,
|
||
now agyeman ... re-record with --update, do not revert the cast."*
|
||
|
||
**Which is the third time in this sequence that a thing was true only because nobody had
|
||
tested it.** The generator comment, the costed restraint, and now the coupling — each
|
||
stated confidently, each wrong, each found by somebody trying it rather than reading it.
|
||
|
||
## R-268 — the sentence that names a player's character, guarded
|
||
|
||
CLEAN GROUND prints "the four-player cut is three effective attackers, not four. Ashcroft
|
||
is not one." It is a claim about a fight, sitting in a document, with nothing checking it.
|
||
Re-kit him, or change what the column dodges at, and the warning goes false while the page
|
||
keeps printing it — and this one is worse than a wrong statistic, because a GM reads it
|
||
aloud to decide who a real player spends four hours being.
|
||
|
||
**`tools/effective-attackers.mjs` measures it per attack, and the distinction is the whole
|
||
entry.** Counted per fight, Braithwaite takes 36% of the cut's disables and reads as its
|
||
best attacker. He is its most armoured: downed in 23.8% of fights against 33 to 37 for the
|
||
others, so he gets a third more swings, and per attack he is the **weakest** of the three.
|
||
|
||
| | attacks/fight | lands | damage/hit | attacks that disable | downed in |
|
||
|---|---|---|---|---|---|
|
||
| Bhattacharya | 6.8 | 17.8% | 5.6 | **13.5%** | 33.1% |
|
||
| Renshaw | 7.5 | 17.3% | 5.6 | **13.1%** | 37.1% |
|
||
| Braithwaite | 9.9 | 16.2% | 4.6 | **11.1%** | 23.8% |
|
||
| Ashcroft | 9.2 | 1.0% | 2.3 | **0.1%** | 34.7% |
|
||
|
||
A rate that rewards survival is the confound R-259 was caught by. Both are recorded; only
|
||
the per-attack column is reasoned from. **Ashcroft is not idle — he makes 9.2 attacks a
|
||
fight and lands one in a hundred**, which is worse for a player than being plainly
|
||
sidelined, and is the sharper form of the warning the scenario already carries.
|
||
|
||
**What it asserts.** Exact drift on every figure, then the sentence itself: three of the
|
||
four clear 5% of attacks disabling and one does not, and the one who does not is Ashcroft.
|
||
The bar sits in a gap of two orders of magnitude, so it tests the claim and not the
|
||
sampling. Proved by giving Ashcroft an armed role in a throwaway worktree: the drift check
|
||
names him, and **re-recording does not silence it** — the claim check then refuses with
|
||
"4 of the cut's 4 clear 5% ... rewrite the warning rather than re-recording."
|
||
|
||
**One reader for the cast, not two.** `tools/declared-cast.mjs` now holds the marker
|
||
reading both scenario guards need. Two copies would be the duplication check-rules exists
|
||
to forbid, and would let the two guards disagree about who is in the fight.
|
||
|
||
**It was called `scenario-cast.mjs` for about four minutes.** `check-scenarios` sweeps
|
||
`tools/scenario-*` into the scenario corpus, so the file was read as a scenario, and the
|
||
`<!-- cast: ... -->` example in its own header comment was parsed as a real cast
|
||
declaration — check-rollable failed with `cast declares "..."`, which is not on the duty
|
||
roster. A file named after what it does, colliding with a convention nobody had written
|
||
down. Renamed.
|
||
|
||
**And `update-readme` was hiding the failure.** It collects each guard's line by running
|
||
it, and dropped any guard that exited non-zero — so check-rollable vanished from the
|
||
README's block, the count word fell to "thirteen" while fourteen guards ran, and the
|
||
document got *quieter* as the build got worse. A missing line is now fatal. That is the
|
||
third variant of this defect found inside the tooling written to prevent it, which is
|
||
starting to look less like bad luck than like where the defect lives.
|
||
|
||
## R-269 — the fourth quadrant, and a mechanism I had described wrongly
|
||
|
||
Three readings of the cut so far took first blood and the outcome together: the party
|
||
strikes first and sweeps (seed 5), the column strikes first and wipes it (seed 2), the
|
||
column strikes first and loses anyway (seed 17). Seed 16 is the missing corner — the party
|
||
takes first blood in round 1 and is wiped — and it runs **24 rounds**, which is longer than
|
||
any fight anyone will run at a table.
|
||
|
||
**What a disable actually does.** I have twice written that a disabling hit "removes an
|
||
attacker". It does not. `locationEffectsFor` charges 30 points off physical or manipulation
|
||
per disabled limb, so Bhattacharya goes from 40 to 10 in round 6 and keeps attacking at 10
|
||
for four more rounds; the creature that loses a leg drops from 50 to 20 and keeps coming.
|
||
For a 40% attacker that is three quarters of their hit chance, which is why the measured
|
||
effect is as large as it is — but it is a slope, not a cliff, and the disabled keep acting,
|
||
keep drawing attacks and keep being targets. R-266 and `first-blood.mjs` corrected.
|
||
|
||
**The last five rounds of seed 16 are the scenario's warning, played.** Neil is dropped in
|
||
round 21 by a critical — `armour 4->0`, because a critical ignores armour entirely — and
|
||
from there it is **Dominic alone against one creature, rolling at 1%, three times, and
|
||
then dying**. Rounds 22, 23 and 24 contain four player actions and not one of them could
|
||
have succeeded. That is what "three effective attackers, not four" costs at the table when
|
||
the fight goes long: not a quieter character, a player rolling dice that cannot come up.
|
||
|
||
**What the fourteen guards would and would not have caught here.** check-firstblood says
|
||
the first blow is worth 40 points, and this fight is squarely in the 25.5% that go the
|
||
other way; the guard is describing a tendency and correctly did not fire. check-attackers
|
||
has Ashcroft at 0.1% of attacks disabling, and rounds 22 to 24 are what 0.1% looks like
|
||
from a chair. Neither guard is wrong, and neither one would have told me a 24-round fight
|
||
was possible — no guard measures the tail, because every one of them averages.
|
||
|
||
**So the reading is still doing work the measuring cannot.** Fourteen guards, and the
|
||
thing worth knowing about this fight — that it ends with one player failing three
|
||
impossible rolls in a row — is visible only by watching it happen.
|
||
|
||
## R-270 — a rule I invented, and the coincidence that hid it
|
||
|
||
R-266 said "the major-wound threshold is half of max HP and it is the same rule in both
|
||
directions". The scenario session carried the same sentence, went to `rules.mjs` to verify
|
||
my correction of a *different* error in it, read the neighbouring functions, and found this
|
||
one. It is not a rule. Verified here rather than accepted:
|
||
|
||
| what I said | what `rules.mjs` says |
|
||
|---|---|
|
||
| a fighter goes down at half maximum hit points | `conditionFor` (1623) — down at `hp <= 2` or unconscious; dying at `hp <= 0` |
|
||
| half max HP is the threshold that matters | `majorWoundFor` (32) is `ceil(hp/2)` and feeds only `dyingLimitFor` — how long the dying last |
|
||
| one rule, both directions | a LOCATION is disabled by `resolveLocationHit` (556) at `after >= locationMax`, which is `locationMaxHp` (54) — a fraction of total hit points, per location |
|
||
|
||
**The coincidence is the interesting part.** For a 10-point neighbour, `majorWoundFor` is 5
|
||
and a leg, abdomen or chest holds exactly 5. For a 12-point agent both are 6. So on the
|
||
locations that get hit most, the invented rule and the real one return the same number, and
|
||
the narration prints `MAJOR WOUND · disabled` together on the same line — two rules firing
|
||
at once, reading as one. Arms and heads are where they part (4 against 5 for a neighbour),
|
||
and I had not looked at an arm.
|
||
|
||
**And the hit table is weighted toward the agreeing case**, which the scenario session
|
||
spotted and I verified against `locationsFor`: legs, abdomen and chest take **12 of the 20
|
||
melee results and 15 of 20 ranged**, and those are exactly the four locations where the two
|
||
thresholds coincide. So it is not that the invented rule happened to be right about the
|
||
common case — narration shows the agreeing case 60 to 75 per cent of the time *by
|
||
construction*. Two independent readers checked a false rule against play and play agreed
|
||
with them, three times in four.
|
||
|
||
So the sentence was not a guess. It was a generalisation from the case where two unrelated
|
||
thresholds agree, checked against the output, and confirmed by it. That is a worse failure
|
||
than a guess, because it came with evidence.
|
||
|
||
**Neither error is neutral in the same direction.** A disable that removes a fighter, and a
|
||
down-threshold at half hit points, both make the fight easier than it is: fewer attackers
|
||
still standing, and agents falling at 6 rather than fighting to 3. A GM reading either would
|
||
have run a party-ending encounter as a survivable one.
|
||
|
||
**And this is the fourth time today that checking one claim surfaced a different one.** Not
|
||
the same claim re-examined — the neighbour of it. The pattern is worth naming: the cost of
|
||
verifying something is mostly the cost of getting to where it lives, and once you are there
|
||
the next function along is nearly free to read. Both of us have now paid that fare and come
|
||
back with something we were not looking for.
|
||
|
||
## R-271 — the tail, and a 40-round cap that scores a long fight as a draw
|
||
|
||
Fourteen guards all average, so none of them can see how long a fight might run. EXPOSURE
|
||
publishes a median and a GM reads it as the shape of the encounter. Measured across 6000
|
||
fights per configuration, three seeds, the shared stream:
|
||
|
||
| | median | 75th | 90th | 95th | 99th | longest | 15+ rounds | 20+ | 30+ |
|
||
|---|---|---|---|---|---|---|---|---|---|
|
||
| four-player cut | 11 | 14 | 19 | 22 | 30 | **71** | 24.0% | 8.2% | 1.1% |
|
||
| six against six | 14 | 19 | 27 | 32 | 45 | **79** | 45.0% | 24.5% | 7.1% |
|
||
|
||
**The six-a-side line is the longer fight, not the cut** — median 14 against 11, and nearly
|
||
half of them run past fifteen rounds. More bodies on both sides means more of them fighting
|
||
on at reduced skill rather than dropping, which is R-270's correction showing up as a
|
||
duration. The cut is shorter because it is decisive, not because it is safer.
|
||
|
||
**Length predicts death.** In the cut, 61.6% of fights that reach fifteen rounds are wipes
|
||
against 42.0% of shorter ones; in the line, 22.4% against 8.9%. A fight that has not
|
||
resolved is not a stalemate, it is a fight the party is losing slowly.
|
||
|
||
**The defect: `maxRounds = 40` is the default every guard runs at, and a fight that reaches
|
||
it is scored as neither a wipe nor a win.** It stops and reports whoever is standing. That
|
||
censors **0.2% of cut fights and 1.87% of the line's** — the line's 99th percentile is a
|
||
true 45 rounds, not the 40 the cap reports, and the longest fight in 6000 runs is 79.
|
||
|
||
**Measured before proposing anything about it, because the fix is four re-recorded
|
||
baselines.** Running first-blood's own measurement at both caps on one stream: the cut's
|
||
swing moves 39.8 to 39.9, the line's 14.8 to 15.1. Against a recorded swing noise of 2.1
|
||
that is nothing. **So the cap stays.** It is a real censoring, it is documented here, and
|
||
correcting it would churn every baseline in the suite to move a figure by three tenths of a
|
||
point. The honest position is not that the cap is right but that it is cheaper than the
|
||
cure, and that a later measurement which cares about the tail must pass its own
|
||
`maxRounds`.
|
||
|
||
**Not guarded, deliberately.** Nothing prints these numbers. A baseline recording figures
|
||
no document cites is a maintenance obligation protecting no claim — and this suite has
|
||
fourteen guards precisely because each one was built when something started being asserted.
|
||
If the tail reaches a page, it gets a guard the same day.
|
||
|
||
---
|
||
|
||
## R-272 — the tail reached a page, so it got a guard; and two guards to stop prose drifting
|
||
|
||
R-271 ended "if the tail reaches a page, it gets a guard the same day." Tim asked for the
|
||
tail, the citation fix and the rules.mjs note together, so this is that day. Guards fifteen
|
||
and sixteen, and one comment.
|
||
|
||
**check-fight-tail (15), on `tools/fight-tail.mjs`.** The suite could not see a long fight
|
||
because every guard in it averages. EXPOSURE printed medians for the same reason.
|
||
|
||
The cap is the measurement here, so the tool passes its own — 400, not `runFight`'s default
|
||
40 — and `--update` refuses to record anything that reaches it. Proved by lowering it back
|
||
to 40: the cut loses 20 fights to the ceiling and the six-a-side 117, with 107 ending
|
||
neither won nor wiped, and the recorder stops rather than writing a truncated distribution.
|
||
That is R-271's defect turned into a refusal.
|
||
|
||
The claim checks read a fresh measurement rather than the baseline, so `--update` cannot
|
||
silence them. Two claims, chosen because the obvious one does not discriminate — "the long
|
||
end is twice the median" is true of both encounters and so distinguishes nothing:
|
||
|
||
- **Length predicts death.** A fight past fifteen rounds ends in a wipe materially more
|
||
often than a shorter one. A GM can act on that: a fight still running is not a stalemate,
|
||
it is one the party is losing slowly.
|
||
- **The six-a-side fight is the LONGER one**, median 14 against 10.7, which is R-270 showing
|
||
up as duration. A disabled fighter does not leave, they fight on 30 points down, so more
|
||
bodies means more people swinging badly for longer. The cut is shorter because it is
|
||
decisive, not because it is safer.
|
||
|
||
**check-cited (16), on `tools/check-cited.mjs`.** check-firstblood and check-attackers catch
|
||
the game changing; neither reads the document. Re-record after a re-cast and the artifact
|
||
updates, the guard goes green, and the paragraph goes on printing the old number with a
|
||
citation under it saying where the new one lives — the citation making it worse, because it
|
||
tells a GM the figure is checked.
|
||
|
||
So the citations are machine-readable: `**40**<!-- cite: first-blood cut.swing -->`, resolved
|
||
against the named JSON on every build. 25 of them now. It earned its place immediately by
|
||
failing three times on its first runs — a config keyed `column` that the prose called `line`,
|
||
two figures rounded from 32.3 to 32, and a vacuous pass when zero citations were found, which
|
||
is now fatal in its own right.
|
||
|
||
**It also refuses to let a field be quoted at all.** `fight-tail.longest` is recorded for
|
||
drift and may not reach prose: same party, same seeds, same runs, and renaming a config from
|
||
`line` to `column` moved it from 71 to 90 rounds, because `seedFor` derives the stream from
|
||
the id. Measured across seven labels — median spread 0, p95 1, p99 3, **longest 21**. A
|
||
sample maximum reads exactly like a bound on the encounter and is a property of the label.
|
||
EXPOSURE states the long end as p99 instead, which is stable to about three rounds.
|
||
|
||
The same discipline caught the deadlier ratio: 2.42 at six a side with a seed spread of 1.1,
|
||
forty-five per cent of its own value. The document states the direction of that effect and
|
||
explicitly declines to quote its size; `deadlierNoise` is recorded so the next reader can see
|
||
why.
|
||
|
||
**rules.mjs.** A comment over `resolveLocationHit` recording that its two thresholds are
|
||
unrelated and usually agree. `disabled` is a fraction of the pool per location; `majorWound`
|
||
is ceil(hp/2) and feeds only `dyingLimitFor`; neither takes anybody out of a fight, which is
|
||
`conditionFor` at 2 hit points or a destroyed head. For a 10 hp human, leg, abdomen and chest
|
||
capacities are 5 and majorWoundFor is 5 — and those locations take 12 of 20 melee results and
|
||
15 of 20 ranged. Two readers independently reconstructed a rule that does not exist, checked
|
||
it against the log, and were confirmed by it, because the agreeing case is most of the hits.
|
||
The note says: if you are about to describe either threshold from watching a fight, test an
|
||
arm. It is the only place the difference is visible.
|
||
|
||
**What this pass is really about.** Four of the defects found today were inside the guards
|
||
rather than the game. A guard is a claim we have agreed to stop checking, which is its whole
|
||
value and exactly why an assumption inside one is the least likely thing in the repo to be
|
||
questioned. Both new guards are therefore built to refuse rather than to record: one will not
|
||
write a censored measurement, the other will not let an unstable figure be cited. Neither can
|
||
be satisfied by re-recording.
|
||
|
||
## R-273 — the opposed roll the content had been calling for since before the rule existed
|
||
|
||
The hollow man's THE FILE IS YOURS NOW says "opposed POWx5". CLEAN GROUND calls for one
|
||
twice. `slice-text.mjs` uses the word three times. `rules.mjs` — the single authority —
|
||
defined no opposed roll at all, and a scenario was carrying a local ruling that said in
|
||
its own text that it was a ruling and not a rule. That is the honest way to hold a gap
|
||
open and it is not a way to close one.
|
||
|
||
**The rule is the scenario's ruling, generalised, not a new invention.** Both sides roll,
|
||
the better band wins, and it uses only the ladder the game already has. Two changes in
|
||
making it general:
|
||
|
||
*Ties.* The scenario said "equal bands favour the agent", which `rules.mjs` cannot say
|
||
because it does not know who is a player. It now says **ties go to whoever is being acted
|
||
upon** — which `defenceOutcomeFor` has always said, since a defence turns aside a blow of
|
||
its own quality or better. The scenario session checked every opposed beat in its document
|
||
rather than assuming there were two: in both, the antagonist is the aggressor, so the two
|
||
formulations give identical answers everywhere the game actually asks. Its version was
|
||
parochial and read correctly only because no agent ever initiates one.
|
||
|
||
*Unsettled is not a win.* If neither side reaches a success the contest is unsettled —
|
||
`{ winner: null, settled: false }` — rather than a win for the resister. The difference is
|
||
visible at the table, and the scenario needed both halves: the hollow man's swap is a
|
||
repeatable attempt within a scene, and the Act Four countdown row fires once and is marked
|
||
permanent. The rule carries the distinction and deliberately does not say which applies.
|
||
|
||
**Two things the scenario session required, both of which changed the shape.** It asked
|
||
for both sides' levels in the return, so a scenario can price a fumbled swap without this
|
||
file deciding what a fumble costs; and it asked for per-side difficulty, because its Act
|
||
Four beat is *Ashcroft resists at Difficult* — 85 to 42 — and a rule taking two flat
|
||
ratings would have forced `applyDifficulty` back into its prose, which is the arrangement
|
||
check-rules exists to forbid. So there are two exports: `opposedOutcomeFor` compares two
|
||
graded levels, and `opposedContestFor` runs the whole thing from ratings and rolls through
|
||
`resolveBands` and `gradeRoll`. Neither redefines anything.
|
||
|
||
**Spot-checked against a real beat rather than an invented one.** The Act Four contest is
|
||
in check-rules: POWx5 55 against 85 at Difficult, which resolves to 42, and the tie goes to
|
||
Ashcroft. 112 rules, 413 formulas.
|
||
|
||
**And it reaches a page, because a rule nothing reaches is this project's oldest defect.**
|
||
Page 1 of the player rules now has "When two people want opposite things", and it states
|
||
the rule by ASKING it — the tie-break sentence and the margin example are computed from
|
||
`opposedOutcomeFor` at build time, so the book cannot drift from the engine. That is the
|
||
arrangement the rest of that document already uses and the reason it has never been wrong.
|
||
|
||
`docs/scenarios/` is untouched by me. The local ruling is the scenario session's to delete,
|
||
and removing the line that says the game has no opposed roll would have been the obvious
|
||
courtesy and exactly the wrong one.
|
||
|
||
## R-274 — the opposed roll works, and the combat never reaches it
|
||
|
||
Played the hollow man against the four-player cut to see the new rule in use. It never
|
||
fired. The fight is five rounds of knives and dodges, the hollow man goes down in round
|
||
five, and **THE FILE IS YOURS NOW — the power the bestiary prints, the reason the opposed
|
||
roll exists — does not appear at all.** `simulate.mjs` reads statblocks and weapons; it has
|
||
no concept of a creature's tactics. So R-273 closed a gap in `rules.mjs` and left the same
|
||
gap one layer out: the rule is reachable from the sheet, the API and the rulebook, and not
|
||
from the only thing in this project that plays the game.
|
||
|
||
That is the fourth version of this defect in the log and the first one I built myself while
|
||
writing an entry about the previous three.
|
||
|
||
**Resolved by hand, the way a GM would, the rule behaves.** Every roll pair enumerated —
|
||
100 x 100, exact rather than sampled — for both beats that call for it:
|
||
|
||
| Act Three, hollow man POWx5 100 | takes them | resisted | nothing |
|
||
|---|---|---|---|
|
||
| Braithwaite, POWx5 60 | 48.0% | 50.0% | 2.0% |
|
||
| Renshaw, 70 | 40.2% | 58.3% | 1.5% |
|
||
| Bhattacharya, 75 | 36.3% | 62.5% | 1.3% |
|
||
| Ashcroft, 85 | 28.4% | 70.8% | 0.8% |
|
||
|
||
**The tie-break carries the rule.** At 100 against 60 the aggressor succeeds on all but the
|
||
96-00 override and still only wins 48%, because both sides succeed most of the time and an
|
||
equal band goes to the person being acted upon. That is the rule doing what it was written
|
||
to do: a catastrophic outcome is hard to impose even by something far stronger than you.
|
||
|
||
**Act Four prices THE OFFER, and the number is worth having.** The understudy is POWx5 75,
|
||
not 100, and the countdown reaches for the lowest POW in the room:
|
||
|
||
| Act Four, understudy POWx5 75 | takes them | nothing |
|
||
|---|---|---|
|
||
| Braithwaite, the lowest POW, 60 | 37.6% | 10.0% |
|
||
| Ashcroft having REFUSED the offer, 85 | 21.9% | 3.8% |
|
||
| Ashcroft having ACCEPTED it, Difficult, 42 | **48.7%** | 14.5% |
|
||
|
||
Accepting THE OFFER takes Ashcroft from the **safest** person in the room to the **least
|
||
safe** — 21.9% to 48.7%, past Braithwaite's 37.6%. The scenario intends that price and now
|
||
knows its size.
|
||
|
||
**One thing the scenario session should see.** The Act Four row fires ONCE and is marked
|
||
permanent — no retry. Against Ashcroft at Difficult, **14.5% of the time neither side
|
||
reaches a success and the row simply does nothing**. Act Three can absorb that because the
|
||
swap is a repeatable attempt in a scene; a once-only climactic beat that no-ops one time in
|
||
seven is a different proposition at a table. The rule is behaving correctly and the beat may
|
||
want a floor. That is the scenario's decision, not the rule's, and it is theirs.
|
||
|
||
Figures here are from an enumeration outside the guarded pipeline. They are exact rather
|
||
than sampled, which is a stronger claim than usual, and they still do not belong on a page
|
||
without a guard.
|
||
|
||
## R-275 — 41 powers, and the harness had never read one
|
||
|
||
R-274 found that the hollow man's THE FILE IS YOURS NOW never fires in a simulated fight.
|
||
It is not alone: **41 statblocks carry a POWER in their tactics prose and `simulate.mjs`
|
||
read none of them.** It reads characteristics and weapons. So a redcap, whose entry says it
|
||
ignores the cumulative defence penalty entirely, has been measured as a creature that tires
|
||
— in check-lethality, in check-focus, and in every figure this project has published about
|
||
it. The defect was never that the powers were unimplemented. It was that **nothing anywhere
|
||
said they were not**, so every number read as complete.
|
||
|
||
**`tools/powers.mjs` classifies all 41 into three, and the third is the point.**
|
||
|
||
| | | |
|
||
|---|---|---|
|
||
| **wired** | 2 | the harness applies it, and `effect` says how |
|
||
| **notSimulable** | 14 | a fight rule the harness cannot express, each with its reason |
|
||
| **outOfCombat** | 25 | not a fight rule — a scene, a social problem, a clock |
|
||
|
||
`check-powers` refuses any POWER that is not in one of the three, and refuses a
|
||
`notSimulable` without a stated reason. A creature added tomorrow cannot arrive
|
||
unclassified, and a gap that stays is a recorded decision rather than a silence.
|
||
|
||
**The guard does not check that the harness IMPORTS a power. It checks that the power moves
|
||
a fight.** Each wired creature is fought with its power and again with it stripped, and the
|
||
two must disagree. A power the simulator reads and does not act on is this defect one layer
|
||
in, and a guard that tested for the wiring would have passed on it.
|
||
|
||
**What moved, and it was exactly the wired two:** the courier's wipe rate 9.4% to 1.0%,
|
||
because THE CONSIGNMENT means it attacks at half rating while it is carrying; the redcap's
|
||
0.7% to 0.9%, small because these fights rarely make it spend a second defence, which is
|
||
itself worth knowing — NOT TIRED is a much bigger rule at a table with a crowd than in a
|
||
measured duel.
|
||
|
||
**One power was wired and then deliberately un-wired, which is the honest part of this
|
||
entry.** ARGENT AND GULES halves what the supporter takes from anything the borough owns.
|
||
Wired, it tripled the supporter's wipe rate — **25.1% to 75.2%** — because the harness has
|
||
no ground and so applied "while it stands on ground the borough holds" unconditionally.
|
||
That is a 50-point swing resting on an assumption the harness cannot state. It is now
|
||
`notSimulable` for the same reason the grindylow's THE PULL is: a power conditional on a
|
||
place cannot be simulated by something with no places in it. I had wired one and refused the
|
||
other on identical facts, and only noticed because the measurement was absurd.
|
||
|
||
**Two baselines re-recorded** — lethality and focus — and the bestiary regenerated from
|
||
them. Seventeen guards became eighteen alongside the scenario session's handouts check.
|
||
|
||
## R-276 — NOT TIRED is visible in play, and worth nothing where we measure it
|
||
|
||
Played four agents against one redcap to watch R-275's wiring fire. Seed 3, round 1, the
|
||
same round:
|
||
|
||
```
|
||
Kate → Redcap A: 9 SUCCESS — dodged (72 vs 75)
|
||
Neil → Redcap A: 33 SUCCESS — dodged (70 vs 75)
|
||
```
|
||
|
||
**Both dodges at 75.** `DEFENCE_STEP` is -30, so the second one would have been at 45 and
|
||
Neil's 70 would have failed. The rule its own entry has always printed is now in the fight.
|
||
|
||
**What it is worth, and where.** Measured with the power and with it stripped, 2000 fights
|
||
x 3 seeds:
|
||
|
||
| | the power costs the party |
|
||
|---|---|
|
||
| one redcap, four agents focusing | **6.1 points** (6.0, 6.3, 6.0) |
|
||
| one redcap, four agents at random | **6.1 points** |
|
||
| three redcaps, four agents | **0.1 points** |
|
||
|
||
**NOT TIRED is a rule about being outnumbered.** A lone redcap is attacked four times a
|
||
round and spends four defences, so exemption from the stacking penalty is most of its
|
||
survival. Three redcaps against four agents are attacked roughly once each, never spend a
|
||
second defence, and the power is worth a tenth of a point.
|
||
|
||
**Which is why check-lethality barely moved.** ~~Its redcap figure went 0.7% to 0.9%
|
||
wiped... the lethality baseline fights redcaps in a pack, and the pack is precisely the
|
||
configuration in which this power is worth nothing.~~
|
||
|
||
> **CORRECTED, and the correction is worse than the claim.** `check-lethality` fights every
|
||
> creature **solo** — `measure(party, [spec])`, with a comment two lines up saying so:
|
||
> *"Solo, because a creature is the unit under test — counts are an encounter's business."*
|
||
> I asserted it fought packs from memory of a different guard. It is **check-focus** that
|
||
> records a pack size, and its redcap is `n: 3` — so the "measured where the power does
|
||
> nothing" point is true, and true of the other guard.
|
||
>
|
||
> And the lethality figure was never hiding anything. Wipe rate moved 0.7% to 0.9% because
|
||
> **one redcap cannot wipe four agents whatever it ignores** — that column is at its floor.
|
||
> The column that moved is *agents down*, **0.55 to 0.74 of 4**, which is a 35% relative
|
||
> increase and is not small at all. I read the small number, explained it, and did not look
|
||
> at the one next to it.
|
||
>
|
||
> Two statistics, two guards, and I had them crossed in both directions. Found by being
|
||
> asked to re-record a baseline that was already recorded the way I said it was not.
|
||
|
||
**One thing I nearly presented as evidence and did not.** At seed 3 the party wins with the
|
||
power on and is wiped with it off — the opposite of the power's direction. That is not a
|
||
finding, it is two diverging streams: change one roll and every subsequent fight is a
|
||
different fight. A single seed shows what happened, never what a rule is worth, which this
|
||
log has now learned twice in one day and would have learned a third time here.
|
||
|
||
## R-277 — n=3 is right for the redcap, and R-276's explanation of why was wrong twice over
|
||
|
||
Asked whether `check-focus` records the right pack size for a redcap. The frozen party
|
||
against n of them, 1000 runs x 3 seeds:
|
||
|
||
| n | spread | focus | focus gain | without NOT TIRED | the power is worth |
|
||
|---|---|---|---|---|---|
|
||
| 1 | 98.9% | 98.9% | 0.0 | 99.3% | 0.4 |
|
||
| 2 | 73.4% | 78.3% | 4.9 | 77.1% | **3.8** |
|
||
| 3 | **28.0%** | 34.3% | **6.3** | 31.1% | 3.1 |
|
||
| 4 | 3.7% | 7.3% | 3.6 | 4.2% | 0.6 |
|
||
| 5 | 0.3% | 0.7% | 0.4 | 0.5% | 0.2 |
|
||
| 6 | 0.0% | 0.1% | 0.1 | 0.0% | 0.0 |
|
||
|
||
**n=3 is correct, on the criterion the guard actually uses and on two others.** `pickSize`
|
||
takes the size nearest an even fight: |28.0 − 50| = 22.0 against n=2's 23.4. It is also
|
||
where focus fire is worth most (6.3, clear of its 1.4 noise), and it is one of the two
|
||
sizes where the creature's power does anything.
|
||
|
||
**But the margin is 1.4 points and the scan is 400 runs.** Running `pickSize` as the guard
|
||
runs it across eight seeds: **n=3 seven times and n=2 once**. So the recorded pack size for
|
||
this creature is one seed in eight away from being 2, and the recorded gain would move 6.3
|
||
to 4.9 with it. The baseline is right and it is not *robustly* right, which is a property of
|
||
a 1.4-point margin rather than a defect in the choice.
|
||
|
||
**AND R-276'S WHOLE EXPLANATION WAS WRONG.** It said NOT TIRED "is a rule about being
|
||
outnumbered", from a table showing 6.1 points against one redcap and 0.1 against three.
|
||
Both figures were measured against **the CLEAN GROUND four-player cut**, which R-276 never
|
||
named, and against the frozen party the pattern inverts: a lone redcap is worth **0.4**, not
|
||
6.1, because that party wins 98.9% of the time and nothing can show an effect against a
|
||
ceiling.
|
||
|
||
The real pattern is the one the guard's own `MEASURABLE` band already encodes: **the power
|
||
is worth most where the fight is in doubt** — 3.8 at n=2 and 3.1 at n=3, falling to nothing
|
||
at both ends. Not outnumbered. Undecided. I invented a mechanism that fitted two points from
|
||
an unnamed party, and it was wrong in the same shape as the `--mode` citation this morning:
|
||
a figure that is correct for the configuration I ran and stated as though it were general.
|
||
|
||
**Three corrections to one entry in one day.** R-276 claimed a guard fought packs when it
|
||
fights solo, read the wipe column when the agents-down column was the one that moved, and
|
||
explained a power by a mechanism that does not hold. Each was found by somebody asking me to
|
||
act on it. The entry is worth keeping in that state rather than tidying: it is the clearest
|
||
example in this log of confident, internally consistent, repeatedly wrong.
|
||
|
||
## R-278 — raising SCAN_RUNS found that SCAN_RUNS had never been read
|
||
|
||
R-277 left the redcap's recorded pack size one seed in eight from being 2 instead of 3, so
|
||
the instruction was to raise the scan until it stopped flipping. Measured rather than
|
||
guessed: still unstable at 3000 and 4000, stable across twenty seeds at 6000. Raised it.
|
||
|
||
**Then the re-record took fifteen seconds, which was impossible.** Fifteen times the scan
|
||
work cannot cost what 400 runs cost, so I read the call:
|
||
|
||
```js
|
||
const winRate = (foes, targets, seed) => { for (let i = 0; i < RUNS; i++) ...
|
||
...
|
||
const w = winRate(Array(n).fill(spec), "random", seedFor(...), SCAN_RUNS);
|
||
```
|
||
|
||
**Three parameters, four arguments.** JavaScript discards the fourth in silence, so every
|
||
scan this guard has ever run used `RUNS`, and `SCAN_RUNS` — declared, commented, passed —
|
||
has never been read by anything. Changing it from 400 to 6000 also did nothing, which is
|
||
how it surfaced: the fix I was asked to make was inert in exactly the way the thing it was
|
||
fixing was.
|
||
|
||
That is the defect this project names as its own signature — a value computed and never
|
||
read — living inside a guard, undetected by seventeen other guards, for as long as the
|
||
scan has existed. **No guard can catch it.** The constant is used at its one call site and
|
||
the call site looks correct; only the clock disagreed.
|
||
|
||
**What the honest scan changed.** `winRate` takes `runs` now, and the re-record costs 49
|
||
seconds instead of 15:
|
||
|
||
- **the redcap is still n=3, gain still 6.3** — the answer R-277 gave was right, and is now
|
||
arrived at stably rather than luckily.
|
||
- **the_arrears drops out of the measurable band**, 31 packs to 30. Its best size was chosen
|
||
on 1000 runs and does not survive 6000, which means it has been carrying a recorded focus
|
||
gain that the scan would not now stand behind.
|
||
- **the_committee moves n 2 to 6** and stays pinned either way, so nothing is measured there
|
||
and nothing changes.
|
||
|
||
**The claim check still passes**: focus fire helps in all 30, all of them clear of their own
|
||
noise. Bestiary regenerated, and it prints 30 where it printed 31.
|
||
|
||
**The lesson is not about arity.** It is that the only signal was a number being too small,
|
||
and I nearly did not look — a fifteen-second re-record is good news, and good news is what
|
||
nobody investigates. The same instinct that made the README quieter as the build got worse
|
||
(R-268) made this cheaper as the scan got weaker.
|
||
|
||
## R-279 — what the_arrears was claiming, and why dropping it is the wrong kind of right
|
||
|
||
R-278's honest scan pushed the_arrears out of check-focus's measurable band. It is already
|
||
re-recorded — `{ n: 2, pinned: 14.4 }` where it held `{ n: 2, spread: 14.7, focus: 21,
|
||
gain: 6.3, noise: 1.5 }`. So the question is what that row was asserting and whether it was
|
||
true.
|
||
|
||
**It was asserting that concentrating fire on two of the arrears takes the party from about
|
||
15% to about 21%. Measured independently at 4000 runs x 3 seeds, it is:**
|
||
|
||
| n | spread | focus | gain | spread noise |
|
||
|---|---|---|---|---|
|
||
| 1 | 85.7% | 85.7% | 0.0 | 1.7 |
|
||
| 2 | **14.2%** | **20.4%** | **6.2** | 2.6 |
|
||
| 3 | 0.4% | 0.8% | 0.5 | 0.1 |
|
||
| 4 | 0.0% | 0.0% | 0.0 | 0.0 |
|
||
|
||
**The claim was true and it reproduces.** 6.2 against the recorded 6.3, from separate code,
|
||
at four times the sampling. The gain is 2.4x its own noise and 44% of the base rate. Nothing
|
||
about the measurement failed.
|
||
|
||
**What failed is a threshold.** `MEASURABLE` is [15, 85] and the scan's estimate of a
|
||
boundary value moved from 14.7 to 14.4. The row was dropped because a number crossed a line,
|
||
not because the effect stopped existing.
|
||
|
||
**And the creature is the reason the line is in the wrong place for it.** the_arrears is a
|
||
step: one of them and the party wins 85.7%, two and it wins 14.2%, three and it is 0.4%.
|
||
There is no pack size that gives an even fight, and the band's two endpoints fall exactly in
|
||
the gap. That shape is itself the most useful thing to know about the encounter — *one is a
|
||
conversation, two is a party-ender* — and the band's effect is to record nothing at all
|
||
about a creature whose defining property is that it has no middle.
|
||
|
||
**I am not moving the band to 14.** Fitting a threshold to the one datum it excludes is how
|
||
a guard stops being a test. But the band is a proxy: it exists because "a fight the party
|
||
wins 100% or loses 100% cannot show an effect of any size", and the direct form of that
|
||
question is already in the artifact — **does the gain clear its own noise?** the_arrears
|
||
answers yes at 2.4x while sitting outside the band, and a proxy that disagrees with the
|
||
thing it proxies for is worth replacing rather than tuning. That is a change to how 47
|
||
creatures are selected, so it is a decision rather than a fix, and it is not mine to take
|
||
unasked.
|
||
|
||
**Nothing published quoted the lost row.** The bestiary prints the above-noise count from
|
||
the artifact, so it moved 31 to 30 on its own and says nothing that is now false.
|
||
|
||
## R-280 — the band replaced by the test it was standing in for
|
||
|
||
`MEASURABLE = [15, 85]` was a proxy. The question it answered is *can this fight move at
|
||
all* — a rate pinned at 0 or 100 cannot show an effect of any size — and it answered with
|
||
two round numbers. R-279 found it excluding the_arrears by three tenths of a point while
|
||
that row carried a gain of 6.2 at 2.4x its own noise.
|
||
|
||
**The direct form, in the units this guard already uses:** a fight is measurable if its
|
||
spread-fire rate stands clear of both ends by more than its own seed-to-seed noise.
|
||
|
||
**It deliberately does not look at the gain.** Admitting the sizes where focus fire happens
|
||
to clear its noise would make this guard's own claim true by construction, which is the
|
||
worst thing a guard can be. Size is still chosen on nearest-an-even-fight, and admission is
|
||
still decided without reference to the effect being measured. The order changed — the row
|
||
is measured first and admitted second, because the noise the test needs is a product of the
|
||
measurement rather than of the scan.
|
||
|
||
**31 measurable became 38.** the_arrears is back at gain 6.3, and seven others arrive:
|
||
|
||
| | n | spread | gain | noise |
|
||
|---|---|---|---|---|
|
||
| switchboard | 6 | 86.3% | **8.1** | 1.4 |
|
||
| the_arrears | 2 | 14.7% | **6.3** | 1.5 |
|
||
| nuckelavee | 2 | 8.2% | **5.9** | 0.5 |
|
||
| hollow_man | 6 | 92.2% | **4.6** | 0.8 |
|
||
| supporter | 2 | 4.5% | 2.6 | 0.8 |
|
||
| hedley_kow | 6 | 97.7% | 1.6 | 0.6 |
|
||
| the_stanchion | 2 | 6.0% | 0.2 | 0.9 |
|
||
| the_choir | 6 | 99.3% | 0.2 | 0.4 |
|
||
|
||
Four of the eight were carrying effects larger than most of the rows the band already
|
||
admitted. **switchboard at 8.1 is the second largest gain in the artifact and the band was
|
||
throwing it away**, because an 86.3% fight is one point past a round number.
|
||
|
||
**And the honest part: the last two do not clear their own noise.** the_choir has 0.7 points
|
||
of headroom at 99.3% and used 0.2 of it; the_stanchion has six points of room and used 0.2.
|
||
The above-noise count falls 31 of 31 to 36 of 38, and the bestiary now prints 36. That is
|
||
not a regression — it is two measurements saying *no detectable effect*, which is a result
|
||
the band suppressed by refusing to take it. A guard that only records the cases where
|
||
something happens is not measuring, it is confirming.
|
||
|
||
**What is now weaker, and worth watching.** The hard claim — focus fire helps in every
|
||
measurable pack — is satisfied by gains of 0.2, so it is a weaker sentence than it was when
|
||
the band admitted only contested fights. If it should be a stronger claim, the place to
|
||
strengthen it is the claim, not the admission rule; tightening admission to protect a claim
|
||
is how the band got here.
|
||
|
||
## R-281 — a ratchet, because "helps in all of them" survives the advice becoming useless
|
||
|
||
R-280 left check-focus asserting that focus fire helps in every measurable pack, and that
|
||
sentence is now satisfied by a gain of **0.2**. True, and thin: the advice could decay to
|
||
nothing everywhere it is not already decisive and the guard would still pass.
|
||
|
||
**The strengthening is a ratchet rather than a threshold.** The artifact records how many
|
||
measurable packs showed a gain clearing their own noise — `reliable: { aboveNoise: 36, of:
|
||
38 }` — and the check refuses if that SHARE falls. It may rise freely.
|
||
|
||
A threshold here would be a number I chose, and choosing one to sit just under the current
|
||
value is the move that produced `MEASURABLE = [15, 85]` in the first place. A ratchet has no
|
||
chosen number in it: the bar is the measurement, and it moves only when the game does.
|
||
|
||
**It is a share and not a count**, so it cannot be satisfied by admitting more packs — which
|
||
matters, because R-280 just widened admission from 31 to 38 and a count-based ratchet would
|
||
have been paid off by the widening.
|
||
|
||
**Proved by trying it three ways**, in throwaway worktrees:
|
||
|
||
- *Make focus fire actively bad* — target the healthiest enemy instead of the most hurt.
|
||
The drift check fires first and names four creatures, which is correct: any movement is
|
||
drift before it is anything else.
|
||
- *Make it unreliable and re-record* — concentrate only half the time, then `--update`.
|
||
The older claim fires: focus fire now helps in 37 of 38, so the page's flat assertion is
|
||
already false and the ratchet is never reached.
|
||
- *Leave the game alone and claim a better past* — set the recorded share to 38 of 38. The
|
||
ratchet refuses: *"clears its own noise in 36 of 38, down from 38 of 38 ... that is a
|
||
change in the game, not a baseline to refresh."*
|
||
|
||
The middle case is the useful one to understand: **the ratchet only bites where the existing
|
||
claim does not**, in the band between "still helps everywhere" and "helps as reliably as it
|
||
did". That band is exactly where a slow degradation lives, and it was unguarded until now.
|
||
|
||
## R-282 — the bestiary is now checked against the powers, including the next one
|
||
|
||
R-275 wired NOT TIRED into the simulator and the bestiary went on listing the redcap among
|
||
"the ones that take the most stripping" — the exact opposite of what its own entry says
|
||
three pages down — with **eighteen guards green over it**. `check-bestiary` proves the page
|
||
matches its generator; the generator had never heard of `powers.mjs`. Two files agreeing
|
||
with each other while both disagree with the engine is not a check, it is a quorum.
|
||
|
||
**check-powers now asserts what the page must say, per effect kind.**
|
||
|
||
- `defenceStacking` — the creature must not appear in the stripping list, must be named as
|
||
off the ladder, and the section's "across N creatures that spend defences" count must
|
||
equal the dodgers minus the exempt. Arithmetic tying prose to `powers.mjs`.
|
||
- `attackFactor` — the page must print the rating the simulator actually uses, as a number.
|
||
"HALF the rating above" is a rule a GM has to do arithmetic on and a guard cannot read;
|
||
**40%** is both. The courier's entry now carries it.
|
||
|
||
**The part that matters is the failure on an UNKNOWN kind.** A wired effect with no rule in
|
||
`DOCUMENT_RULES` fails the build by itself:
|
||
|
||
> `effect "reachBonus" has no rule about what BESTIARY must say. Add one to DOCUMENT_RULES:
|
||
> an effect the harness applies and the page never mentions is how the redcap came to be
|
||
> listed under advice that does not work on it.`
|
||
|
||
Without that clause this guard would cover the mistake already made and nothing else —
|
||
which is the failure mode of most guards written the day after a defect. The next effect
|
||
kind cannot reach the simulator without somebody deciding what the document owes it.
|
||
|
||
**Proved three ways** in a throwaway worktree: the redcap put back on the ladder fails with
|
||
both the list and the missing exemption named; the courier's real rating removed fails; a
|
||
new `reachBonus` effect fails for having no page rule. Exit codes read directly.
|
||
|
||
**What this does not cover, stated so it is a decision rather than a silence.** The
|
||
assertions are textual — they check that the page says a thing, not that what it says is
|
||
true. A `damageFactor` rule is registered as a no-op because nothing wires one today, and
|
||
it will need writing when something does; the guard will demand it, but it cannot write it.
|
||
|
||
## R-283 — a reworded sentence and a missing one are different failures
|
||
|
||
R-282 checked the bestiary's prose with `String.includes`, which has one bad failure and it
|
||
is the one this suite keeps finding elsewhere: **a guard that reports the wrong thing
|
||
confidently.** Reword the exemption sentence and the message read "BESTIARY never says it is
|
||
off the stripping ladder" — sending a maintainer to look for a sentence that is sitting
|
||
right there, while the real problem (a pattern that has quietly stopped reading) goes
|
||
unmentioned.
|
||
|
||
Each textual rule now reads the page twice. **Strict** is the sentence as it stands, and
|
||
tighter than before: `**Name is not on this ladder at all.**` with its bold and its full
|
||
stop, not the bare clause a substring would have accepted. **Loose** is the same claim in
|
||
any wording. Three outcomes, three messages — strict passes; loose-only is named a REWORD,
|
||
quotes the line it found, and says the page may well be right; neither is the omission.
|
||
|
||
**The loose test was itself wrong first, and the way it was wrong is the point.** Anchoring
|
||
the courier's rule on "a sentence containing 40% and 80%" matched its own statblock line —
|
||
`**Attacks with** Grasping limb at 40% ... **dodges** 80%` — so *deleting* the sentence
|
||
outright reported a reword and quoted the statblock back as the surviving claim. Confident
|
||
and wrong, one level below the defect the two-tier read exists to prevent. The loose anchor
|
||
now excludes that generated marker, which makes it a test of the claim rather than of the
|
||
digits; reword the statblock too and this degrades to OMISSION, the safe direction.
|
||
|
||
Four discriminations proved in a worktree, message read in each: reword kept → REWORD;
|
||
line deleted → OMISSION; wrong number, right wording → mismatch naming both figures; redcap
|
||
back on the ladder → the R-282 contradiction, unchanged.
|
||
|
||
## R-284 — the third tier: a figure that moved is not a sentence that went
|
||
|
||
R-283 left a case falling through to the omission branch: the sentence intact, the number
|
||
inside it wrong. The message said BESTIARY "does not state the rating the simulator actually
|
||
uses" about a page that was stating one. It named the right figures, so it pointed at the
|
||
fix, but it described a document that does not exist — the same class of confidently-wrong
|
||
report R-283 was written to remove, one case further in.
|
||
|
||
`reads` now runs four tiers: **strict** (the sentence as it stands) passes; **shape** — the
|
||
strict pattern with its value slot loosened — reports a MISMATCH and says which of the two
|
||
numbers moved; **loose** reports a REWORD; nothing reports the OMISSION. The mismatch text
|
||
is the sharpest of the four on purpose: *a figure the page states and the harness
|
||
contradicts is worse than one it omits, because a GM can act on it.*
|
||
|
||
**Scoped while adding it.** The rules read `page`, and every one of them is the only rule of
|
||
its kind today, so a page-wide match found the right line by luck. Wire a second
|
||
`attackFactor` creature and the courier's rule would have matched that creature's sentence
|
||
and reported the courier correct — a guard passing on someone else's evidence. Per-creature
|
||
rules now read the creature's own `### Name` entry. Proved by deleting the courier's line and
|
||
planting an identical one in the redcap's entry: still OMISSION, where before scoping it
|
||
would have passed.
|
||
|
||
Five cases proved in a worktree, message read in each: effective rating wrong, base rating
|
||
wrong, both wrong, reworded with the numbers kept, deleted. Plus the decoy above.
|
||
|
||
## R-285 — the ladder rules read the ladder, and the owning slice is not always the entry
|
||
|
||
R-284 scoped the factored-rating rule to `### The Courier` and left the defence-stacking
|
||
rules reading the whole page. Same latent defect, other half of the file: the exemption
|
||
sentence and the "Across N creatures that spend defences" count were matched anywhere in
|
||
`docs/BESTIARY.md`, so the guard would accept the claim wherever it happened to sit.
|
||
|
||
Proved rather than assumed. Moving the exemption sentence out of its section and into the
|
||
redcap's statblock entry — the page now silent exactly where a GM reads the stripping advice
|
||
— **passes at R-284** and fails after this change. Same for the counted line.
|
||
|
||
**The owning slice is not the creature's entry, and assuming it was would have broken a
|
||
correct page.** A factored rating belongs to the creature: `### The Courier` owns it. The
|
||
ladder exemption does not — "stripping is not a plan against it" is an answer to the
|
||
paragraph it sits in, and the sentence is generated into `## Shooting at something that
|
||
moves`. Scoping that rule to `### Redcap` would have failed a document that is right. So
|
||
`sliceOf` takes a heading at any level and returns it down to the next heading of the same
|
||
or higher level, and each rule names the slice that owns its claim.
|
||
|
||
Renaming the section itself now fails by name — *BESTIARY has no "## Shooting at something
|
||
that moves" section, which is where the defence-stacking rules are checked* — rather than
|
||
silently scoping to nothing, which is the shape of every guard that passes because it found
|
||
nothing to check.
|
||
|
||
Five cases in a worktree: two decoys (fail here, pass at R-284, the pass confirmed by running
|
||
HEAD's copy against the same tree), a reword inside the section, the renamed heading, and the
|
||
clean control.
|
||
|
||
## R-286 — a declaration and a mention of one look identical to a regex
|
||
|
||
Swept the other seventeen guards for the page-wide match R-285 removed from check-powers.
|
||
Most are already scoped or have nothing to scope. Two are not, and the first is live.
|
||
|
||
`CLEAN_GROUND.md` contains **two** things matching `<!-- cast: ... -->`: the declaration
|
||
under `## Casting`, and — ten lines below it — the warning that explains it, which quotes
|
||
`` `<!-- cast: -->` `` inside backticks and matches the same pattern with an empty capture.
|
||
`check-rollable` and `declared-cast.mjs` both used `.match()`, first hit wins. **The whole
|
||
arrangement has been correct because the declaration happens to come first.**
|
||
|
||
Move that warning above the list and `check-rollable` reads a cast of nobody, falls back to
|
||
`ROSTER_BEST`, and goes green having held every skill in the document to the full duty roster
|
||
instead of the declared six — the identical OK line, the scope silently widened. Instrumented
|
||
and read off, not reasoned about:
|
||
|
||
[scope] docs/scenarios/CLEAN_GROUND.md -> the duty roster (warning moved above)
|
||
[scope] docs/scenarios/CLEAN_GROUND.md -> its declared cast of 6 (as the file stands)
|
||
|
||
`check-firstblood` fails on the same page, but says *"CLEAN GROUND now casts 0"* — describing
|
||
a scenario that casts six, which sends a reader to the cast list rather than to the two
|
||
markers. R-268 had this bug from the other side, when a tools file's own example was read as
|
||
a real declaration; this is the same defect facing inward, and the marker that triggers it was
|
||
added to the document by the warning telling everyone how load-bearing the marker is.
|
||
|
||
`castMarkersIn` now blanks code spans and fenced blocks before scanning — padded, so line
|
||
numbers still point at the source — and more than one surviving marker is fatal with every
|
||
line named. `check-rollable` imports it rather than carrying a second regex, so the guard that
|
||
checks the cast and the guards that measure the fight cannot disagree about who is in it.
|
||
|
||
**Disclosed to c0 the same minute.** The comparison that found this was meant to run in a
|
||
throwaway worktree; `git worktree add` failed on a stale registration, the heredoc ran on in
|
||
the shared tree, and it moved one line of c0's uncommitted `CLEAN_GROUND.md`. Reverted by
|
||
hand, `## Casting` block diffed byte-identical against HEAD, c0 told what to check and why
|
||
before anything else was done. A `test -d` on the worktree now guards the pattern.
|
||
|
||
## R-287 — a scope anchored on a name that has moved is the whole file
|
||
|
||
`check-behaviour` scopes three source assertions with `indexOf`. For a name that is gone
|
||
`indexOf` returns -1 and `slice(0, -1)` is everything-but-one-character, so the scope does not
|
||
shrink or fail — it becomes the file. Renaming the end anchor took one test's body from
|
||
**3,184 characters to 30,825** with the assertion still passing: a test that reads like a
|
||
statement about `rollWeaponDamage` making a statement about all of `ringbrp.mjs`.
|
||
|
||
The other site windowed `burstAttack` at 4,000 characters. The function runs 4,305, so the
|
||
window was already short of its own subject, and one edit the other way would have had it
|
||
testing whatever came next for a `locate: false` that belongs to a different call.
|
||
|
||
`bodyOf` now asserts the anchor and ends structurally, at the next top-level function. A
|
||
missing anchor says so — *check-behaviour scopes a test to "export async function burstAttack"
|
||
and ringbrp.mjs no longer contains it. Fix the anchor: unfixed, the test would read the whole
|
||
file and pass* — where the old code reported *"burstAttack still calls rollWeaponDamage"*,
|
||
which is a claim about a call when the truth is a claim about a name.
|
||
|
||
**The rest of the sweep, so the negative result is on the record.** check-cited resolves each
|
||
citation positionally and already refuses markers it cannot read. check-handouts matches each
|
||
almanac row's note against the whole scenario (`md.includes(note)`); the notes are distinctive
|
||
sentences and there is no smaller slice that owns them, so it is left alone deliberately
|
||
rather than overlooked. check-rules builds its covered-set from labels it generates, not from
|
||
searching a document. check-lang, check-creatures, check-templates, check-kits and the
|
||
baseline guards do no document matching at all.
|
||
|
||
## R-288 — an empty declaration is not the same as no declaration
|
||
|
||
c0's note on R-286: the reader is unambiguous now, but the fallback that made the failure
|
||
look like success is still there. It is right, and the fix has to be narrow. **No marker at
|
||
all is the ordinary case** — sixteen of the seventeen scenarios here declare no cast and are
|
||
rightly checked against the whole duty roster. A marker that is present and names nobody is
|
||
a different thing: somebody wrote a declaration, the guard read nothing out of it, and the
|
||
fallback then widens the population silently, so a clue only Pollard can reach passes on the
|
||
strength of an agent the scenario does not cast.
|
||
|
||
Replacing CLEAN GROUND's declaration with `<!-- cast: -->` passes before this change and
|
||
fails after it, naming the line. A scenario with no marker still falls back, unchanged.
|
||
|
||
## R-289 — the cast reader, tested on strings, and the spelling that testing found
|
||
|
||
c0's point after R-286: both cast defects were demonstrated by editing CLEAN_GROUND.md in the
|
||
shared tree to watch what the reader did with it. That is how one session came within a
|
||
`git checkout` of another's uncommitted work — and it is also the weaker test. It holds one
|
||
shape at a time, it leaves nothing behind, and it cannot be run again next month.
|
||
|
||
Seven cases now live in `check-behaviour`, as strings: the real document's shape; the mention
|
||
moved above the declaration (the R-286 bug, which must still read the six); two real
|
||
declarations both seen, with line numbers surviving the code-blanking; a fenced declaration
|
||
ignored; present-and-empty told apart from absent (R-288); spelling; and the odd-marker
|
||
counter.
|
||
|
||
**Writing them found a live one.** `<!-- cast : ... -->` — one space before the colon —
|
||
matched nothing. Not an empty declaration that R-288 would refuse: *no declaration at all*, so
|
||
check-rollable fell back to the whole duty roster and printed its usual OK line having checked
|
||
every skill in the document against people the scenario does not cast. This is check-cited's
|
||
defect exactly, where `Cite:`, `CITE:` and `cite :` all failed open while the guard reported a
|
||
count identical to a clean tree. The reader now takes `cast\s*:`, case-insensitively.
|
||
|
||
**And the asymmetric half, because widening only fixes the spellings we thought of.**
|
||
`castLikeIn` recognises anything comment-shaped containing `cast\w*` that the strict reader did
|
||
not consume, and check-rollable names it: `cast =`, `casting:`, `the cast is` all fail the
|
||
build rather than silently declaring nobody. It is deliberately looser than the reader — an
|
||
ordinary comment near the casting section can trip it, and the message says prose belongs in
|
||
backticks, where nothing here looks at it. A false alarm costs a reword; the opposite error
|
||
costs a guard that checks the wrong six people and says OK.
|
||
|
||
**The tests were then checked for being load-bearing**, by mutating the reader in a worktree
|
||
and reading which of them went red — each mutation asserted to have applied, after a first
|
||
pass where three silently did not and the green that followed proved nothing:
|
||
|
||
| mutation | red |
|
||
|---|---|
|
||
| `cast\s*:` back to `cast:` | spacing |
|
||
| code-span blanking removed | four, including both R-286 cases |
|
||
| marker list truncated to the first hit | two declarations both seen |
|
||
| odd-marker counter disabled | cast-shaped marker named |
|
||
|
||
## R-290 — the same spelling gap in the other two readers
|
||
|
||
Swept every reader that parses a human-written marker, after R-289 found `<!-- cast : -->`
|
||
invisible. Two more had it. The probe is the same each time and it costs a minute: put the
|
||
plausible misspellings to the reader as strings and print what it returns.
|
||
|
||
**`tagsIn` in check-scenarios.** `/\[CUS:\s*([^\]]+)\]/g` — no `i`, no space tolerance.
|
||
`[cus: Spot]`, `[Cus: Spot]`, `[CUS : Spot]` and `[ CUS: Spot]` all read as nothing. This is
|
||
the corpus reader behind check-scenarios' skill validation and check-rollable's reachability,
|
||
so a beat spelled any of those four ways was checked by neither guard while both printed OK.
|
||
Now `\[\s*CUS\s*:\s*...\]`, case-insensitive. **The corpus contains no such tag today** — the
|
||
88-to-89 roll count during this work was c0 writing v0.18, not the widening finding one, and
|
||
HEAD's reader gives 89 on the same tree. Checked rather than claimed, because a preventive fix
|
||
reported as a discovery is a lie about what the tree contained.
|
||
|
||
Worth recording that c0's `outcome-coverage.mjs` loose pattern already catches all four as
|
||
unparsed. Its guard was the only thing between those spellings and silence.
|
||
|
||
**The `POWER:` marker, which nothing covered.** `bestiary.mjs` and check-powers' own
|
||
classification scan both used the literal, so `Power:` and `POWER :` generated no entry line
|
||
and were never reported unclassified. A creature added tomorrow with `Power:` in its tactics
|
||
**passes check-powers green** — 41 powers classified, the forty-second invisible. Measured on a
|
||
clone with its dodge skills stripped, so the dodger-count assertion could not fire and take the
|
||
credit: HEAD OK, this change names the marker and the creature.
|
||
|
||
The reader now takes `POWER\s*:` and stays uppercase, because `power:` occurs in ordinary
|
||
tactics prose and matching it would invent powers. The other half is the counter: anything
|
||
power-marker-shaped, case and spacing free, that the strict reader did not consume. Measured
|
||
against content.mjs before wiring — 41 strict markers, zero loose ones outside them — so it
|
||
starts from a clean corpus rather than from an exception list.
|
||
|
||
**All three fixes share one shape, which is check-cited's from R-283 and is now the house
|
||
pattern**: widen the reader for the spellings you thought of, and add a looser counter that
|
||
names the one you did not. A marker nothing reads is worse than no marker, because whoever
|
||
wrote it believes the thing is declared.
|
||
|
||
## R-291 — the canary c0 asked about, asserted rather than remembered
|
||
|
||
R-290 widened `tagsIn`. c0 asked the right question about it: `outcome-coverage`'s
|
||
`beatLikeIn` names the tags its own reader cannot parse, and if the widened `tagsIn` reaches
|
||
past that loose pattern, the unparsed count drops to zero for the wrong reason and stops
|
||
being a canary.
|
||
|
||
**It does not.** Measured on both real readers rather than by copying either pattern:
|
||
CLEAN_GROUND.md gives 22 beats from `tagsIn`, 22 from `beatsIn`, 0 unparsed. Across spellings,
|
||
everything `tagsIn` accepts is seen by the other guard — parsed, or named. `[CUS Spot]` and
|
||
`[CUS= Spot]` are refused by both strict readers and still named by the counter.
|
||
|
||
"Does not today" is exactly the kind of claim that decays without anyone noticing, so it is a
|
||
test: **a tag one reader accepts is never invisible to the other.** It imports both real
|
||
functions, so neither pattern is duplicated and the test cannot agree with a stale copy.
|
||
Mutation-checked — widening `tagsIn` to accept `[CUS Spot]`, past the counter, turns it red.
|
||
|
||
**What the measurement found that the question did not ask about.** For `[cus: …]`,
|
||
`[CUS : …]` and `[ CUS: …]` the two guards now *disagree*: `tagsIn` reads the tag and checks
|
||
it for reachability, while `beatLikeIn` reports it unparseable and fails the build, because
|
||
`beatsIn`'s own strict half kept the case-sensitive literal R-290 removed from `tagsIn`. Not
|
||
silence — the build stops, which is the safe direction — but the message names the wrong
|
||
thing, and the fix is the one check-rollable already made for the cast: read through the other
|
||
guard's function rather than a second regex. That file is c0's and the call is theirs; it is
|
||
recorded here so it is a decision rather than something nobody wrote down.
|
||
|
||
## R-292 — share where a tag starts, not what it says
|
||
|
||
c0 asked whether `tagsIn` should return indices so `beatLikeIn` could take its spans from it,
|
||
as check-rollable does with `castMarkersIn`. No — and c0 had already reasoned its way to the
|
||
same answer. Returning indices widens this file's signature to serve another file, and
|
||
R-291's cross-reader test already fails the moment the two disagree.
|
||
|
||
But detection is not prevention, and there is a third thing to share that neither of us
|
||
proposed. The two patterns **differ in their bodies for good reasons**: `tagsIn` captures the
|
||
whole tag and lets `skillsIn` split it, `outcome-coverage` stops at the em-dash because the
|
||
outcome text follows. Sharing the whole pattern would be wrong. What was copied into both
|
||
files, and what actually drifted, is the **opening** — the literal `\[CUS:`, widened here by
|
||
R-290 and left behind there, after which three spellings had one guard reading a tag and the
|
||
other calling it unparseable.
|
||
|
||
So `TAG_OPEN` is exported as a string and `tagRe(body)` composes a fresh matcher onto it —
|
||
fresh because a shared `/g` regex carries `lastIndex` between callers, which is its own quiet
|
||
defect. Each reader keeps its own body. The half that must agree is the only half shared.
|
||
|
||
Verified nothing moved: 22 tags before and after, c0's body composed onto `TAG_OPEN` gives the
|
||
same 22 its own pattern does, the four tolerated spellings still read, `[CUS Spot]` still
|
||
refused, 19 guards green.
|
||
|
||
**Also corrected here**: I told c0 the count had gone 21 → 22 because they were writing v0.18.
|
||
Wrong. `beatsIn` returns every marker and `measure()` filters the quoted one — Anomaly Lore at
|
||
line 758, Act Three's quotation of the format. 22 markers, 21 real beats. Both figures were
|
||
right and the disagreement was mine.
|
||
|
||
## R-293 — a record of a session is not a scenario
|
||
|
||
The fifth self-reference case, and the only live one: counting the readers that had broken on
|
||
their own format turned up a sixth that nothing had ever caught. `docs/scenarios/` holds three
|
||
kinds of document — the scenarios, eight desk playtest records, and two art prompt sheets —
|
||
and every guard reading the corpus treated all three as scenarios. Harmless for citations and
|
||
skill spellings. False for reachability.
|
||
|
||
check-rollable's claim is that **every clue a GM can put behind a roll is reachable by somebody
|
||
at the table.** A playtest report quoting `` `[CUS: Anomaly Lore — what a peg is]` `` to say
|
||
which beat a roll hung off had that quotation held to the claim. Six such quotations across
|
||
passes 4, 6 and 7 were being checked as live rolls, and a record of a session already played
|
||
could fail the build over a skill nobody can reach. Measured: planting `Science (Physics)` in
|
||
pass 4 gives *"a clue is behind a roll nobody present can make"* before the split and passes
|
||
after it, while the same skill in CLEAN_GROUND still fails. 89 rolls across 18 files becomes
|
||
83 across 8.
|
||
|
||
**Classified by the document's own H1, not its filename**, because `tools/scenario-*` naming is
|
||
what swept a tools file into this corpus in R-268. A generator has no H1 and is a scenario by
|
||
construction.
|
||
|
||
**An unclassified document is fatal.** An allowlist that silently drops what it does not
|
||
recognise would take a new scenario out of reachability checking on the day it was written —
|
||
the fail-open this session has spent its whole length removing. `classifiedFiles()` is called
|
||
by check-scenarios, the first guard over the corpus, so the message arrives at the right guard,
|
||
and it names the corpus rather than a guard because it runs inside whichever one asked first.
|
||
|
||
**Two things I had wrong on the way**, both caught by insisting on a measurement. The first
|
||
probe used `Demolition` and passed everywhere, which looked like the guard being blind; it is
|
||
reachable, and the playtest files have no cast marker so they were being checked against the
|
||
whole duty roster rather than the declared six. The claim "would have failed the build" was
|
||
hypothetical until a skill nobody on the roster reaches proved it. And the first version of the
|
||
new test ended in `assert.ok(… || true)` — a tautology that passes on anything, which is the
|
||
decoration this suite exists to refuse. Mutation-checked after removing it: making
|
||
`playableFiles` stop excluding records turns it red.
|
||
|
||
## R-294 — a citation that resolves against the rule, not against a stored copy of it
|
||
|
||
c0 asked whether the step-5 contest split should get a baseline, and argued against its own
|
||
suggestion: an artifact that is a pure function of `rules.mjs` is a cache of the rule, and a
|
||
guard over it mostly asserts that arithmetic has not changed. The objection is right. The
|
||
conclusion is not "do not guard it" but **"do not store it"**.
|
||
|
||
`check-cited`'s `ARTIFACTS` map now takes a `{ derive }` entry as well as a file path. A
|
||
string is a recorded baseline — sampled, seeded, noisy, expensive to re-run, and re-recorded
|
||
with `--update`. A derived source is enumerated on this build. Nothing is stored, so nothing
|
||
goes stale and **there is no `--update` able to silence a real disagreement between the
|
||
document and the game.** The document writes `**48.1%**<!-- cite: step5 refused.held -->` and
|
||
does not care which kind it is.
|
||
|
||
`tools/step5-split.mjs` is the first one: all 10,000 roll pairs against `opposedContestFor`,
|
||
no seed and no runs. **Targets derived, never written.** 55 and 60 are the lowest POW×5 in
|
||
the six and in the four-player cut; 42 is `applyDifficulty(85, "difficult")`.
|
||
|
||
**Two rules produce those three numbers and c0 caught me before I collapsed them.** The
|
||
refused rows are "the lowest POW in the room" over a population. The accepted row is not the
|
||
lowest of anything — it is a named exception that takes Ashcroft and drops him to Difficult.
|
||
One derivation covering both would be right today for the wrong reason, and there is now a
|
||
test that fails if someone unifies them.
|
||
|
||
**A tie is fatal**, also c0's: "the lowest POW in the room" stops being well-defined the
|
||
moment two agents share it, and answering anyway would hold the prose against an arbitrary
|
||
pick. This fired for real during testing — raising Okonkwo's POW by one ties him with
|
||
Braithwaite at 60, and the build stopped with both names.
|
||
|
||
**Proved four ways** in worktrees: a correct citation resolves (29 figures, 5 sources); a
|
||
wrong one fails with its own message, which says *nothing was re-recorded and nothing can
|
||
be*; a tie refuses; and raising Okonkwo's POW to 13 moves the lowest to Braithwaite, changes
|
||
`refused.held` from 48.1 to 52.4, and fails the citation that was correct a moment earlier.
|
||
That last one is the whole point — the figure follows the rule.
|
||
|
||
**Two errors of mine, both caught by measurement rather than review.** The blend was computed
|
||
from the rounded rows under a comment saying it used raw proportions, giving 43.7 where the
|
||
document correctly prints 43.6; a comment describing what code ought to do is the defect this
|
||
suite is named for, and this one lasted ninety seconds. And the first exhaustiveness test
|
||
asserted the displayed percentages sum to 100 — they need not: 40.7 + 48.1 + 11.3 is 100.1,
|
||
three independent round-ups, and the document's table carries the same harmless 0.1. The code
|
||
was right and the test was wrong.
|
||
|
||
**Landed unused, deliberately.** No citation markers were placed, because `CLEAN_GROUND.md` is
|
||
c0's and the markers are theirs to place. The mechanism is what this commit is.
|
||
|
||
## R-295 — the counterfactual is derived too, and named as one
|
||
|
||
Preparing to place citation markers on the step-5 figures, five of them matched nothing in
|
||
the derived source: 21.9%, 74.3% and 3.8%, each appearing twice. They are the thing against
|
||
Ashcroft's undiminished POW×5 of 85 — the contest that never happens, because a refusing
|
||
Ashcroft is never the one it reaches for.
|
||
|
||
Act Four prints that pair deliberately and correctly, with a warning, because it is what THE
|
||
OFFER sold and without it a reader cannot see the size of the trade. So it is now derived as
|
||
well, under the name `hadHeRefused`: the document's counterfactual is held to the rule like
|
||
every other figure, and the name says what it is. Quoting these three as odds a GM meets is
|
||
precisely what went wrong in two sentences and a table, and a path called `hadHeRefused` is
|
||
harder to misread than a bare 74.3.
|
||
|
||
Everything in the step-5 prose now resolves to a derived value: `refused.*`, `cut.*`,
|
||
`accepted.*`, `hadHeRefused.*`, `blended.*`, the shares, the targets and the thing's own 75.
|
||
The markers themselves are not placed yet — `CLEAN_GROUND.md` is c0's and they are in it,
|
||
placing 91 `packs` citations of their own.
|
||
|
||
## R-296 — EXPOSURE's tables were censored by a ceiling nobody had looked at
|
||
|
||
Desk pass 9 found the scenario's most consequential table — the one that decides whether a
|
||
GM lets a fight happen — held by nothing: about thirty sampled figures, no artifact, and
|
||
`lethality-baseline.json` covering none of them, since that measures creatures **solo**
|
||
against a frozen four-agent party that is not this cast.
|
||
|
||
Building the baseline found the figures were also wrong.
|
||
|
||
`simulate.mjs:490` calls `runFight(rng, partySpecs, enemySpecs)` with no options, so every
|
||
figure EXPOSURE ever printed was measured at the default `maxRounds` of 40 — and `runFight`
|
||
does not report truncation. Its loop is `for (; round < maxRounds; round++)` and it returns
|
||
whoever is standing when that stops. Truncation was inferable from `rounds === maxRounds`
|
||
and nothing was inferring it.
|
||
|
||
```
|
||
line.column6 14.8% -> 15.2% 34/2000 truncated
|
||
line.hollow6col2 7.8% -> 8.3% 129
|
||
line.hollow6col3 28.7% -> 30.1% 264
|
||
cut.hollow6 58.7% -> 74.9% 578 <- 29% of runs never finished
|
||
```
|
||
|
||
**Fourteen points on the most alarming number in the case**, and the one v0.16 added
|
||
specifically to warn four-player tables. R-270 taught this lesson in `fight-tail`, which has
|
||
carried an `assertUncensored` ever since; EXPOSURE's own tables never got one. The longest
|
||
fight at cap 400 is 120 rounds and 400 and 2000 give identical figures, so the new cap is
|
||
comfortable rather than merely sufficient.
|
||
|
||
`tools/pack-tables.mjs` measures all seventeen configs against the cast **derived** through
|
||
`castAndCut`, and refuses to report or record a truncated sweep rather than recording one
|
||
with a warning — a warning on a number that is already wrong is a number that gets quoted.
|
||
`tools/check-packs.mjs` holds the baseline against the game, so the pair is not a loop:
|
||
`check-cited` holds the prose against the record, this holds the record against the harness.
|
||
Its hard claims read `now` and never `base`.
|
||
|
||
**Two things worth keeping from building it.**
|
||
|
||
`measure()` threads ONE rng through every run rather than reseeding per run. The first probe
|
||
reseeded, got different numbers, and nearly reported the published table as unreproducible
|
||
when it was the probe's own stream that was wrong — the same trap as `seedFor` re-rolling a
|
||
measurement when a config is renamed.
|
||
|
||
And the removed-config test **passed for a bad reason**: `MIN_CONFIGS` is a floor of 14, and
|
||
seventeen minus one is sixteen, which clears it. The test was green and meaningless. That is
|
||
worse than an `assert.ok(… || true)`, which passes on anything and is visible on sight; this
|
||
one passed on the specific thing it was written to catch. A dropped-config check was added
|
||
and re-tested on a row that sits in no monotonic chain, so the failure could not be borrowed
|
||
from a neighbouring assertion.
|
||
|
||
*(Committed as a5c4378 under the subject "R-294", which was already taken by the derived
|
||
source. The number here is the correct one; the commit subject is wrong and is left alone
|
||
rather than rewritten.)*
|