45df79d712e1ebc2e39ca6487efdaf3648f9db03
48
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3b65877a94 |
Tiers say what they are worth, and the ladder is guarded
Measuring the tier knob showed it works — one creature against the four agents check-lethality freezes, deaths per fight climb 0.01, 0.04, 0.32, 0.91, 1.43 across the five tiers for a large brute, and monotonically for hunters and flyers too. It also showed the labels oversell. "Deadly" at tier 3 kills a third of an agent and wipes the team three times in a thousand. A GM reaching for a word like that is owed the figure behind it, and the forge's whole argument is that its output can be interrogated — the tier list was the one part of it that could not be. Each tier now carries a measured `worth` line, printed by `forge list` and under the tier selector in the dialog. check-forge measures the ladder and fails if it ever goes down, across three roles and five tiers. Both the labels and the worth lines are claims about behaviour that any change to the skill bands, the hide base or the characteristic multiplier could falsify without touching a word. Proved by weakening tier 4: three roles report the inversion by name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
09e33907fa |
Forge a creature from inside Foundry
CREATURE_FORGE_PLAN Part 5. A third button on the Actors sidebar, beside New agent and New NPC. Pick what it does, how big it is and how bad it is; the card underneath describes what those settings will actually produce, because the argument for a derivation over a form is that it can be interrogated, and choosing between "brute" and "sentinel" from two nouns is not interrogating anything. The card is itself forged rather than written, so it cannot describe a creature the derivation would not make. generateCreature reads the catalogues out of the packs and calls the same forgeCreature the CLI calls. Natural attacks carry their body-plan declarations into flags.ringbrp.plans so the engine can read them. The plan asks that the panel and the CLI cannot diverge, and importing one derivation was most of that guarantee but not all of it: the two feed it from DIFFERENT catalogues — content.mjs on one side, the built compendium on the other. check-forge now builds the engine's view straight out of packs/weapons and requires both to forge the same creature. It found a divergence on its first run, in 40 of 90 pairs. The fallback took the first usable natural attack from the caller's list, and that list arrives in declaration order from content.mjs and in id order from the pack, so a tiny vesh brute was punching for the CLI and constricting at the table. The derivation may not depend on its caller's iteration order; it imposes its own now. Not yet verified in a running Foundry. The pure derivation is covered by 450 guarded combinations and the catalogue seam by the agreement check above, but Actor.create, the pack reads and the dialog itself are the part check-behaviour says outright it cannot reach. They need a world open, and that has not happened yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4dd72c6248 |
Generated NPCs draw their posting's kit
generateNPC read R.core, R.support and R.label. It never read R.kit, so the
role card's DRAWS FROM STORES promised a Containment officer nine items and
the actor arrived holding a utility knife with a flat naturalArmour standing
in for armour it was never issued.
postings.mjs now owns the decision — drawKitFor(role, tier, {classify}) plus
a draw table per tier — because the engine resolves kit keys against the
compendium and check-generator resolves them against content.mjs, and the two
cannot import each other. Classification comes from catalogue membership and
never from the key: climbing_kit is "Breaching charges" and prybar is "Entry
bar", so anything reading a key's spelling is wrong in that posting first.
The weapon↔skill pairing table is gone. Each weapon item already names the
skill that fires it, so the skill is granted from the weapon, and a generated
NPC cannot hold what it cannot use. THREATS.attacks is now creature-tiers
only; those two keep their natural attacks on top of what they drew.
Armour is an item. naturalArmour drops to 0 on the four human tiers, where
the number was standing in for the item, and stays on the two creature tiers,
where it stacks with drawn armour as locationArmourFor already intends.
check-generator (guard 25) was written first and observed failing on nine
counts against unchanged source. One of its assertions was itself wrong —
it read for the creature-tier restriction after the loop instead of before
it, and so reported the fixed code as broken; re-scoped, it passes on the fix
and still fails against git show HEAD:ringbrp.mjs.
Measured with the new tools/npc-cohort.mjs, 2000 runs, seed 11, against the
party check-lethality freezes. Apex · Containment 68.2% -> 88.5% wipe,
Anomalous · Containment 0.3% -> 19.2%, and Dangerous · Field Lead got weaker
because its posting carries one pistol where the tier used to hand it a
rifle. Full table and three incidental findings in R-310.
Two of those findings are guards, not content. check-rules failed on a
comment that quoted the pattern it hunts for, so comment-only lines are
skipped now. update-readme held the guard list as a hardcoded array whose own
comments recorded it going stale twice; it reads the check script instead,
and so does the count-word rewrite that was separately capped at sixteen.
Not changed, and flagged in R-310: breach_suit and riot_shield declare no
coverage, so they armour every location including the head. The location
counterplay NPC_KIT_PLAN.md relies on does not exist, and that is a balance
decision rather than a side effect of this one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
750cbd257d |
Character sheet: reload button for ranged weapons
A reload icon sits beside the ammo count in the readied-weapon strip and the weapons table, for any weapon with a magazine. It tops the weapon up from its reserve (rules.reloadFor, spot-checked), posts the new count and the weapon's reload time to chat, and warns when the weapon is already full or the reserve is empty. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1e0deb2ef0 |
Attack card: humanoid and quadruped damage buttons
The single Roll damage button becomes two, with person and paw icons. Each rolls damage and then a hit location. A targeted token still uses its own anatomy; with no target the blow is placed on the chosen body plan and shown, with nothing applied. Both buttons disable after either is used. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cdbb2e34ee |
R-273: the opposed roll, built where the authority lives
The content has called for 'opposed POWx5' since before the rule existed -- the hollow man's tactics, CLEAN GROUND twice, slice-text three times -- and rules.mjs defined none. A scenario carried a local ruling that said in its own text it was a ruling and not a rule. Generalises that ruling rather than inventing another: both sides roll, the better band wins, only the ladder the game already has. Ties go to whoever is being acted upon, which is what defenceOutcomeFor has always said; the scenario's 'favour the agent' gave the same answer only because no agent ever initiates one. Neither side succeeding leaves the contest unsettled rather than won, which the two beats need in opposite directions. Two exports at the scenario session's request: opposedOutcomeFor compares graded levels and carries both, so a caller can price a fumbled attempt without this file deciding what a fumble costs; opposedContestFor runs it from ratings and rolls with per-side difficulty, so 'resists at Difficult' does not put applyDifficulty back into a document. Spot-checked against the real Act Four beat: 55 against 85 at Difficult, which is 42. Page 1 states it by asking it -- the tie-break and the margin are computed from the rule at build time, so the book cannot drift from the engine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c3a337314f |
One armourAgainst, in the authority, shared with the harness (R-257)
Left open by R-256. Two rules decided how a hit meets armour — armourAgainst (a critical
ignores it, a special halves it, rounded down) and damageAfterArmour (what is left never
goes below zero). Both lived in ringbrp.mjs, which is the game. Neither lived in
rules.mjs, which is the authority, so the guard that forbids redefining a rule had
nothing to forbid. tools/simulate.mjs applied armour from its own inline copies of both,
as it had since it was written.
They agreed, which is the whole point. Nothing published was wrong and no guard could
have said they were two things rather than one. But every number in BESTIARY.md, every
row of the lethality baseline and every measurement quoted from R-251 onward comes out of
that harness: change how a special hit meets armour and the game changes, the numbers
describing the game do not, and all nine guards still pass. Same shape as
|
||
|
|
f847cb2171 |
The sheet header said 9 while the body said 4 (R-256)
Verifying R-255 on a live sheet found the rule working everywhere it was built and contradicted in the first place a GM looks. The header's ARMOUR field is the hide the creature HAS; on a grounded beast the hit-location table under it read 4 and the header still read 9. Nothing computes off that field, so nothing in the code was wrong. It was wrong at the table. The field stays editable and stays 9 — that is the creature's hide and what a GM types into. Under it, when system.grounded is set, the sheet now says what is in play: DOWN: 4 · CHEST 9, derived from exposedHideFor rather than from a second opinion, with the reason on the tooltip. Verified live: no badge in the air, and grounded the badge and the location table agree at 4 with the forequarters at 9. check-rules forbids redefining a rule by NAME, which misses how this goes wrong in practice — nobody redefines exposedHideFor, they write Math.floor(hide / 2) in the sheet because it is cheaper than an import. I wrote that version first and check-rules passed it. It now fails any file outside rules.mjs that halves a hide inline, naming file and line; check files are exempt because stating the expected value independently is what a test is for. That guard also turned up something I have not fixed, recorded in R-256: armourAgainst lives in ringbrp.mjs rather than rules.mjs, and tools/simulate.mjs has its own inline copy of it. They agree today so no published number is wrong, but every figure in the bestiary and the lethality baseline comes out of that simulator. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c0d33ee81d |
Hide 9, grounded 4, vital exempt — the borough is worth 56 points again
R-254 shipped the belly rule and called +6.4 points "modest on purpose". That read the
wrong statistic. +6.4 measured whether grounding helps; the question was whether the
borough still matters, and the gap between fighting the supporter on borough ground and
off it had fallen from 60.6 points to 44.9. Half damage from every departmental weapon is
meant to be the fight's central problem, and I had quietly taken sixteen points off it
while reporting a small number about something else. Same error as R-251.
Three changes that only work together:
- hide 7 -> 9, restoring the weight on the axis that should hurt
- grounded still halves it, 9 -> 4, which an ordinary round beats
- exposedHideFor now takes the location's kind and leaves `vital` unhalved: a beast on
its belly is not presenting its chest to the floor
Measured: 28.4% on borough ground flying, 68.4% grounded, 84.6% off it — a 56.2 point gap
against 44.9 before. Predicted 28.4 / 84.3 / 55.9 before building.
The composition of the three hide functions lives in prepareDerivedData where no test
could reach it, and check-anatomy's guard 6 is deliberately one-sided so it would not
have caught the vital losing its exemption. check-behaviour now composes the same
expression from the real supporter spec and the real tables. Negative-tested four ways:
removing the exemption, un-baring the wings, removing the halving, and inflating the hide
past what a round can beat each fail it with the right message.
Cost: the supporter goes from fifth to second deadliest creature in the book (2.84 -> 3.38
down, 46.8% -> 69.3% wiped). check-lethality refused the build until that row was
deliberately re-recorded.
BESTIARY.md printed the new hide and said nothing about it halving, because the body-plan
blurb predates the rule. The README said "eight guards" in three sentences above a
generated block listing nine. Both are the signature defect in prose: a number written
down that nothing reads. Both now generate.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
ca040b7926 |
The belly is not armoured (R-254)
R-95 made losing a wing change the animal. R-96 took the hide off the
wing so losing one was possible. Separately each was right and each was
measured. Together they gave the party a soft target and 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 grounding the creature took
the party's chance of winning from 44.9% to 1.4%, while its own tactics
told the GM that grounding it was the fight they could actually win.
Found by measuring something else. Every aimed-shot configuration made
the party worse, and the most aggressive was the worst: aiming at a wing
at -20% grounds it in 99% of fights and HALVES the win rate to 10.1%.
Buying the objective reliably was the fastest way to lose, which only
makes sense if the objective is a trap. So no aimed-shot rule; the
problem was never the lack of one.
It also retires the "+31 points" I reported for grounding at 1.7.3. That
was win-rate-if-grounded against win-rate-if-not — selection bias, since
the parties that grounded it were the ones shooting well. Forcing the
grounding gives the causal value and it was -43. A correlation of +31
and a causation of -43 out of the same mechanic.
The fix: a heraldic beast is armoured the way it is drawn, across the
back and the flanks. On its belly half that hide is no longer in the
way, so exposedHideFor halves natural armour while system.grounded is
set. Hide 7 becomes 3.
flying grounded worth
on borough ground 45.3% 51.7% +6.4
off borough ground 90.6% 97.9% +7.3
Modest deliberately. Grounding should help, not decide — the borough is
still the real answer. Losing its damage modifier was worth +26.5 and
would have made the wing the whole fight.
check-anatomy gains a sixth property: a flyer must not be harder to hurt
on the ground than in the air. The first version probed at a single
4-point round and declared the fix broken, because 4 sits inside the one
band where a bare wing beats a halved hide — below 5 damage the creature
really is better off grounded, above it much worse. Integrating across
1-12 gives 2.83 flying against 3.75 grounded and reads it correctly.
Negative-tested against 1.7.8's behaviour, which it names.
The lethality baseline is unchanged: its simulation never grounds
anything, so system.grounded never comes up there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
e5dc9b5299 |
Hide does not grow on membrane (R-96)
Option H, measured before it was built. Losing a wing already mattered
once it happened — a grounded supporter was 40.5% to be killed against
9.3% for one still flying, a swing of thirty points. It almost never
happened: the wings carried the creature's own six points of heraldic
hide, so folding one meant beating armour six eight times over, and it
occurred in 9% of fights. The rule was reachable, not weak, and every
option that made grounding HURT more moved the win rate by under five
points because you cannot spend a state you never reach.
So the lever is the wing, not the consequence. A location may now be
`bare`: worn armour still covers it — barding a wing works exactly as
before — but the animal's own hide stops at the edge of the membrane.
The supporter's wings are bare, and it gets the toughness back where a
six-hundred-year-old record should carry it: hide 6 -> 7, CON 20 -> 36,
so 22 hit points become 30. STR and SIZ are untouched, so the damage
modifier stays +2d6.
Measured over 20,000 fights against the same four agents:
grounded on borough off borough
before 9% 12.5% win 85.6% win / 14.4% wiped
after 67% 22.1% win 83.5% win / 16.5% wiped
The wing shot becomes the play. Fighting it on borough ground stays a
losing proposition, so ARGENT AND GULES is still the real answer and
grounding improves a bad fight rather than replacing the solution — and
the fight off borough ground keeps its teeth, marginally more lethal
than before rather than less. Bare wings ALONE was the trap: it made
grounding routine and turned the off-ground fight into a 96% walkover
with a 3.8% wipe rate, buying the text at the cost of the encounter.
check-anatomy gains the property this whole thread was about: a plan
whose behaviour turns on losing a wing must not armour that wing with
the creature's own hide, or the rule is decoration. Negative-tested by
putting the hide back.
Also re-records the lethality baseline, which was ALREADY stale: HEAD's
own code reproduced 27 of its 47 entries differently, because the
supporter joined NPCS in
|
||
|
|
77465baa30 |
A flyer with a hole in a wing is a quadruped (R-95)
The supporter's tactics have said this since it was written: "It is a flyer, and a flyer with a hole in a wing is a quadruped: disable either wing and it is on the ground for the rest of the scene, which is the fight the agents can actually win." Playing a full combat against it showed the sentence was decoration. `flightLost` was computed, printed on the NPC sheet as GROUNDED, and read by nothing. Both wings disabled cost 20% off PHYSICAL rolls, and Brawl is a MELEE category, so woundPenaltyFrom returned 0 and a grounded supporter attacked at exactly the 75% and exactly the 1d6+1+2d6 it had in the air. In the played fight it was grounded from round five and went on to kill one agent, knock out a second and leave a third dying. Losing a wing now changes the animal. A plan declares what it becomes — winged says `grounded: "quadruped"` — and hitLocationRoll drops the creature onto that plan the moment a wing is disabled or destroyed. The wings leave its hit chart and the six ranged faces they covered go back to the body: vital hits rise from 3 faces in 20 to 5. Every other wound moves across on the R-94 remap with its severity intact. The wing's own damage is deliberately dropped rather than carried, and cleared in its own update so the remap never sees it. A ruined wing must not arrive as a ruined foreleg — the creature is not lamed, it is earthbound, and the injury has already been paid out as the change of shape. check-anatomy gains a fifth property: a declared landing must exist, must have no wings of its own, must not itself be a flyer, must have room for every wound the flyer was carrying without two collapsing onto one location, and must not heal a disabled location on the way down. Three induced failures confirm it fires; the third — landing somewhere with too few locations — passed at first because I had not stated the capacity property, and the guard was sharpened until it caught it. Measured, and reported honestly: this does NOT deliver the fight the tactics text promises. Over 4,000 simulated fights the win rate moves 12.7% -> 12.1% on borough ground and 85.8% -> 86.3% off it, because grounding only happens in 9% of fights there and arrives late. What actually decides this encounter is ARGENT AND GULES: 11.4% win on borough ground against 85.4% off it. The wing rule is now real and mechanical, but the creature's advice still points the GM at the wrong lever. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4a80cc1eca |
Body plans keep what a wound MEANT, not just its number (R-94b)
1.7.1 carried wounds across a change of body plan by moving the raw points. Playing it showed that is the wrong invariant. A location is disabled at its maximum and destroyed at twice it, those maxima come from `frac`, and the plans disagree — an arm is 0.35 of the body and a foreleg 0.42 — so moving the number silently changed the meaning. Measured across the 20 plan changes: 234 severity flips, 138 of them turning a location that was merely disabled into a destroyed one. A destroyed arm arrived as a working-ish foreleg; two disabled arms stacked onto one Vesh grasping limb and arrived destroyed. So severity is now the invariant. A hurt location arrives hurt, a disabled one disabled, a destroyed one destroyed; the points are rescaled to the destination's own maximum to keep that true, and two wounds sharing one location take the worse rather than the sum, because relabelling a creature's anatomy must not destroy a limb that nothing in play destroyed. Correspondence is now stated rather than inferred. Every location gains a plan-independent `slot` (lowerR, upperL, vital, head...), and the remap matches on slot first. The previous version inferred it from `kind` plus table order, which inverted left and right on every crossing: a wounded right arm came back as the opposite hind leg. Where a destination has no matching slot the fallback prefers the same side and the same height, so a wing becomes a foreleg rather than a hind leg and a left wing becomes a LEFT arm. The vesh and the cadence are not bilateral — one grasping limb, five interchangeable units — so their slots carry no side, which is a fix to my own first pass: giving them sided slots was a category error the side guard caught. check-anatomy now proves all four promises over every ordered pair and severity band: nothing is stranded, severity is preserved, left never becomes right and fore never becomes hind between bilateral plans, and no update deletes a wound it is also setting. 432 carried wounds, 0 flips, down from 234. Five induced failures confirm each assertion fires; two of my first attempts at those did NOT fire and were sharpened until they did. Played through in a live world: a church grim filed as a humanoid with a destroyed right arm, a disabled left arm and a hurt chest becomes a dog with a destroyed right foreleg, a disabled left foreleg and hurt forequarters, and converts back to exactly the wounds it started with. Its penalties correctly stop being manipulation and start being movement — a man with two ruined arms drops his weapon, a dog with two ruined forelegs cannot run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a90c4f3e71 |
Carry wounds across a change of body plan (R-94)
locationDamage is keyed by location id and prepareDerivedData reads only
the keys belonging to the current plan, so changing speciesProfile on a
wounded actor stranded every wound it had. Found on a live Barghest that
had been shot in the leg and chest as a humanoid and then switched to
quadruped: 5 points under `legR` and 7 under `chest`, exactly its 12
missing hit points, sitting under keys no quadruped location answers to.
It read on the sheet as a badly hurt dog with eight pristine locations.
Stored and never read — the same defect this system keeps producing.
rules.mjs gains remapLocationDamage, which carries wounds across by KIND,
because kind is what the rules act on. Exact-kind matches are claimed
first in table order, so the obvious pairings land before any fallback
competes for them; that ordering is what makes the arms become the
forelegs going one way and the forelegs become the arms coming back.
Overflow past a location's destroyed ceiling is reported, not dropped.
It also gains locationDamageReplacement, which exists because the first
version of this fix had the bug it was fixing. Foundry merges object
updates so the outgoing keys must be deleted by name (R-93), but a
deletion and an assignment of the SAME key do not both apply — the
deletion wins. quadruped and winged share every location but the wings,
so switching between them emitted `-=hindLegR` alongside `hindLegR: 5`
and threw the wound away. Caught in live testing, not by reading.
Guards, both negative-tested before being wired in:
- check-anatomy now walks all 20 ordered plan pairs and asserts every
wound lands on a location the destination has, the arithmetic closes,
the round trip does not leak, and no update both sets and deletes the
same key.
- check-rules gains 13 spot-checks over both new rules.
Verified in a running Foundry: baseline -> quadruped -> winged ->
quadruped -> baseline conserves all 12 points with no orphans, returns
to exactly {legR: 5, chest: 7}, and whispers the GM a receipt of what
moved each time.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
ddc4f99ff4 |
Hit locations reach ordinary combat, and creatures get a body
THE DEFECT. locationFor had ONE caller in the engine, hitLocationRoll, and hitLocationRoll was called from fall, burn, detonate and burstAttack. Falling, fire, explosions and automatic fire. Nothing else. rollWeaponDamage rolled dice, printed a number and returned it, so the chat Damage button, the character sheet's damage button and the item sheet's all stopped there. A grenade knew which arm it took off. A sword did not. Every individual piece was already correct - the tables, per-location armour and coverage, disable and destroy thresholds, Major Wound, dying, death, the figure on the sheet. They simply were not joined up, which is why this reads as a wiring change rather than a new subsystem. - rollWeaponDamage now carries through to hitLocationRoll: it rolls the location, applies that location's armour, applies the wound, and handles the rest through the machinery that already owned it. Nothing here re-implements any of that - locationModeFor picks the melee or ranged column from the weapon's class. The two columns have always differed and nothing was choosing between them - with no target it does exactly what it did before and SAYS SO on a card, rather than failing quietly - burstAttack opts out with locate:false - it rolls its own location per round, and letting both fire would have applied every round of a burst twice BODY PLANS. Two new tables: quadruped (four legs, fore/hindquarters, neck, a head that is hard to reach) and winged (that, plus wings). Forequarters are the vital. DISABLE EITHER WING AND IT CANNOT FLY, which is the fight a party can win. Barghest, Kelpie and Church grim were all species "baseline" - a black dog, a horse and a grave-hound, each hit-located with two arms and a chest. Fixed. Added "The supporter", a winged heraldic beast, so the new table is exercised by real content instead of existing unused. Body plan is a NEW FIELD, not a species. A bear is not a playable people with characteristic dice and talents, so creature specs take bodyPlan and the playable species list is untouched. THE NPC SHEET had no hit locations at all - 102 lines against the character sheet's 627 - while every NPC in the game had them derived and wired to consequences. It now carries the body-plan selector, the figure, the per-location table, the condition and a GROUNDED flag. The sheet class already extended the character sheet, so it already HAD the context; the template never used it. FOUND IN PASSING, by the new guard: kind "body" - the abdomen, the trunk, the hindquarters - did nothing whatever. Its printed effect has always promised "-30% to all Physical actions and bleeding 1 hit point per round until First Aid" and the code delivered none of it, on every body plan including the humanoid default. It now bleeds 1, or 2 destroyed, exactly as bodyX says. NEW GUARD: check-anatomy. Every body plan must cover 1-20 exactly once in BOTH modes, every location must be drawn and every drawn shape must be a location, and every kind must be one locationEffectsFor acts on, has effect text for, and has a destruction outcome. Verified it fails on a d20 gap, a shapeless limb and an unhandled kind before wiring it in. Ninth guard. VERIFIED IN A RUNNING FOUNDRY, not just unit-tested: melee bar reads the Melee column and the rifle reads Ranged on the same creature; forequarters graded as a vital hit; a maxed wing set flightLost and the sheet said GROUNDED; the real chat Damage button rolled 1D20 15, found the right foreleg, applied 12 through armour, disabled the leg and took the dog from 16 to 4; and with nothing targeted nothing was touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
94479641b3 |
simulate: locate the damage
The harness measured a game in which every blow went to a general pool. That was
tolerable while every creature was a baseline human and merely wrong once the bestiary
had a Cadence in it: a creature whose entire mechanical identity is being whittled down
body by body was being measured as a person with an odd portrait, and the number it
printed was about nobody.
locationFor and resolveLocationHit move from ringbrp.mjs to rules.mjs, and the
category mapping inside woundPenaltyFor becomes woundPenaltyFrom beside them, for the
reason applyDifficulty and resolveBands moved before them: node cannot load the engine,
and the harness may not own a second copy of a rule. All three arrive with the
spot-checks they never had - 103 rules now, 346 formulas verified.
What the loop does now: 1d20 against the DEFENDER's own species table, melee finding
limbs and shooting finding centre of mass; damage to that location and to the general
pool, so the two agree; disabled at the location maximum and destroyed at twice it,
cumulatively. Then the consequences actually apply - a destroyed head is unconscious
immediately (conditionFor has always accepted that flag and was never passed it, so a
headshot used to leave the target swinging), a ruined arm costs -30 to every attack
including shooting, a ruined leg costs -30 to dodge, and each Cadence body lost costs
the whole creature -10 to everything.
Verified as mechanics rather than as numbers that moved: no species can return a
location it does not have across all 40 rolls; vesh and cadence have no head at any
roll and cadence has no vital either; a vesh ridge is hit 3/20 where a human head is
1/20, which is the "long target rather than a small one" the tables were designed for.
A built Cadence has five bodies at 4 points each and no vital; a built Vesh has six
locations with the ridge the largest.
Measured consequences, seed 11, 400 runs, four duty-roster agents:
keepers x6 31.0% -> 36.5% wiped
the_choir x11 0.3% -> 0.0% wiped, and now degrading as bodies go rather than
being a person who cannot be shot in the head
long_walker 42.0% wiped, which nothing had ever measured
Still no bleeding, panic, Coherence, cover, range bands or fire modes. Still a floor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
255b146753 |
Species anatomy moves where node can read it
RINGBRP.locationTables and RINGBRP.bodyArt lived inside ringbrp.mjs, which no tool can import — it touches game, Hooks and foundry at load. So nothing outside Foundry could ask how many hit locations a Cadence has, or what shape a Vesh is, which is exactly what art generated from a statblock needs to know. Moved byte for byte to anatomy.mjs and assigned back, the same move rules.mjs got when the simulator needed the combat rules. One definition, two readers. check-rules matched those tables with a regex against ringbrp.mjs and went blind the moment they moved — it reported "could not read locationTables" and the species check silently stopped running. It imports the value now. A guard that parses source text breaks when the source is refactored; one that imports it cannot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2c547efe72 |
simulate: a lethality harness that uses the real rules
THROUGH TRAIN publishes "MEASURED, 400 runs each" and a 24% wipe rate; the starter publishes its own table. Nothing in this repository could produce either number, so both were claims rather than results and neither could be re-checked after a rule moved. The harness knows no rules of its own. Everything comes out of rules.mjs, because a harness with its own copy of the combat loop measures a game nobody is playing and drifts silently the first time a rule changes. Two things had to move for that to be possible: applyDifficulty, resolveBands and gradeRoll -> rules.mjs. The whole of d100 resolution lived in ringbrp.mjs, which node cannot import. All three are pure, so they moved and the engine re-exports them; no caller changes. check-rules then refused them for having no spot-check, so they now have fifteen, covering the 1% floor, the 96-99 rule and 00-always-fumbles. They had none in their entire life inside the engine. expandFromRegister -> tools/expand-spec.mjs. A roster agent is written as a job and a rank and carries no skills or kit; everything it can do is derived at build time by a function locked inside build-packs.mjs, which cannot be imported because importing it runs the build. A simulated agent expanded by a second copy of that logic is not the agent that gets packed. Moved verbatim; both sides import one copy. Deterministic throughout: same creature, party and seed, same numbers, which is what lets a published table be verified rather than remembered. What it finds: for the Act Three standoff the seed barely matters (0.5-2.8% wipe across five seeds, eight rounds throughout) and party composition dominates everything. Four support agents against the six miners wipe 92.8% of the time; swap in the heavy and the warden and it is 0.3%. The published 24% is not a property of the encounter, and no single figure can be. Models Reaction re-rolled each round, banding, graded defences, the stacking defence penalty, damage by band, major wounds and the dying clock. Does NOT yet model hit locations, bleeding, panic, cover, range bands or fire modes, all of which make a losing fight worse. Read the output as a floor, never a ceiling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
24b1aaa682 |
Development: skills improve from what beat you, not what worked
Reversed experience. A skill is marked when it FAILS - failure or fumble - and never when it works, and the improvement roll is the same reversal: roll OVER your rating and the skill goes up 1d6. Nobody learns anything from the lock that opened first time. The two halves pull against each other on purpose, so ratings converge instead of running away. Simulated over 2000 campaigns: a 10% and an 85% skill start 75 points apart and are 11 apart after 60 cases. - rules.mjs owns it: DEVELOPMENT, developmentMarksOn, developmentImproves, developmentCeiling, developmentGainFor - trainingCap has been on the skill schema and the item sheet since it was written and nothing has ever read it. Development reads it now: a skill stops at its own cap, and everything else stops at DEVELOPMENT.ceiling, because a d6 gain could otherwise push a 98% skill past 100 - GM opens the phase from a GM-only toggle in the token controls; it posts a card naming every agent and their marked count, and puts a Development button on each sheet. Players press their own - the toggle re-reads the setting on change, since getSceneControlButtons only fires at load and the button would otherwise lie if the phase were opened by the API or a second GM - rules journal and player primer rewritten from the tables; the old text described the pre-reversal rule on both halves - 7 spot-checks, 4 behaviour tests; verified end to end in a running Foundry Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
37b4043746 |
Play a full case, then fix what it found
Ran THE HOLLOW ROAD with five agents — approach, crossing, contact, the thing on
the road, extraction, debrief — with panic and Coherence both live. It ran clean
and it found one thing that mattered.
ANOMALY LORE 0. Marta Holloway, Field Lead, Senior Field Officer, could not roll
the department's signature skill. Nor could Okonkwo. Induction was gated to
TRADES on the reasoning that a career posting is itself the agency training —
but 17 of the 25 postings do not list anomaly lore, its printed base is 0 on
purpose, and nothing else supplies it. So the people who joined the department
straight out of school were the only ones who never received its basic training,
which is exactly backwards.
Induction is a FLOOR for everybody now, posting or trade, in the live generator
and the pack builder both. It never demotes: Bhattacharya keeps Registry's 63,
Holloway goes 0 → 43, Okonkwo 0 → 33. All twenty built agents verified non-zero.
Two GM tools the case showed were missing, both found by feeling the friction
rather than by reading anything:
- clearPanic(actors). Panic clears when the scene does — one line in the book
and, until now, five sheets edited by hand while the table waited. It leaves
Coherence alone, which is the whole point of having two tracks.
- partyState(). Everybody's hit points, panic and Coherence in one card. With
five agents the alternative is asking "what are you on?" five times a scene,
which is friction a table absorbs silently and then blames on the game.
And the player-facing gap: the primer never mentioned panic. Players got a
one-page aide-memoire for a track they now meet in the first scene. It has a
page of its own and a row on the cheat sheet, both generated from the same
tables as the rules.
One thing I reported as a bug and withdraw: Okonkwo's "Firearm (Pistol) 5%" was
me rolling the wrong weapon. He carries a marksman rifle at 53%. check-kits was
right.
Version 1.2.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
7ec7efd29b |
Replay the scene with the fumble rule; expose panicReliefFor
Ran the east corridor again with fresh dice and every relief attempt routed
through easePanic, fumbles included.
The rule did not fire this time. All three attempts failed cleanly — Psychology
58 against 40, and two POW×5 rolls at 66 and 76 — so nobody was punished for
trying. That is worth reporting as loudly as a hit would have been: two
play-throughs in and a fumbled relief has come up once, which is about right for
something meant to be a memorable disaster rather than a tax.
Braithwaite still broke at 5 on "it said your name" and fled. Marrow went down
in a single round to a special with the entry bar — 19 through armour — against
the 4.5-round average the thirty-run tuning gave, so that was dice, not balance.
Then the question the rule actually raises. Relief rolls are made at your own
panic penalty, so the worse you are the worse your odds AND the wider your fumble
range — and now a fumble adds a point. That is a feedback loop, so I measured it
rather than trusting it, 300 runs per row:
from 2, POW×5 60% 280 calm · 0 broke
from 3, POW×5 60% 227 calm · 2 broke
from 4, POW×5 60% 138 calm · 31 broke
from 4, POW×5 50% 96 calm · 35 broke
from 4, POW×5 85% 267 calm · 5 broke
Recovery dominates everywhere. The worst case — overwhelmed, weak POW, no help —
spirals into breaking about one time in eight, which is a bad night rather than a
trap. No change needed.
Also: panicReliefFor and panicAfter were missing from the API, which I only
noticed because measuring the loop needed them and the alternative was
re-implementing the rule in the test — the exact duplication check-rules exists
to forbid. Both exposed.
Version 1.1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
5aaae186e0 |
A fumbled relief attempt puts a point on
Trying to steady somebody and making it worse is the most familiar thing on that list, and until now it was the only outcome that cost nothing. Done as a NEGATIVE relief rather than a second code path: panicReliefFor returns -1 on a fumble, and easePanic already subtracts what it is given, so subtracting a negative raises the track. One number, one direction, no branch that could drift from the other. The clamp still holds — fumbling at 5 stays at 5. The chat card had to be told, because otherwise it prints the word "Psychology" over a number that has just gone UP and the table reads that as a bug rather than as the rule. It now leads with a red FUMBLED tag. Guarded three ways: a spot-check in check-rules for the -1 itself, and two behaviour tests — one that the sign survives easePanic's subtraction and actually raises the track, one that it cannot push past the top. A test on the constant alone would have passed even if easePanic had healed on a fumble. Verified live: 2 → fumble → 3 → success → 2 → special → 0, and a fumble at 5 staying at 5, with the cards reading correctly. Version 1.1.2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cbad87ba98 |
Add Occult, Religion and Psychology, and the Panic track
From the first game's list. The three skills are not decoration — they are the
levers on the new track, which is why they arrived together.
PANIC, 0 to 5, up. It is the other half of Coherence and the rules page says so
in the first line, because a table that confuses them will make the wrong call
at the worst moment:
Coherence what knowing has cost you weeks 10-0, down
Panic what is happening to you NOW minutes 0-5, up
A trigger gives a point with NO ROLL. Asking for a die to find out whether you
are frightened slows the scene and tells the player how to feel; the die is for
getting rid of it, which is the interesting direction.
0 steady · 1 rattled · 2 shaken -5% · 3 losing it -10%
4 overwhelmed -20% · 5 broken
The first point is free on purpose, so the opening shock of a scene costs the
table nothing. At 5 you break — freeze, flee or fixate, choose fast — and come
back at 3, not at 0, because breaking is not a rest.
Relief is where the skills come in. A success takes one point, a special takes
two. Psychology is the only thing that steadies somebody who is NOT you.
Religion steadies you alone and does not care whether you are right. Occult is
the odd one: naming a thing correctly, out loud, helps everybody who can hear —
the department's actual work, and the only relief that reaches a room. Plus
POW×5 to steady yourself, and your anchor object once per case, which is what
that briefing scene was for.
The three skills are placed in six registers rather than left floating: Occult
to the professor and to Registry, Psychology to the doctor and to Interview,
Religion to Antiquities and Pathology.
Guards, because a track nothing reads is this project's oldest failure: the five
new rules carry spot-checks in check-rules, and check-behaviour gains nine tests
— the free first point, monotonic penalties, only 5 breaking, clamping at both
ends, breaking not clearing the track, special taking two, every band/trigger/
relief having a label, and every relief naming a skill the catalogue actually
has.
Verified in a live game: penalty reaches the roll (58 → 58 → 53 → 48 → 38 for
panic 0-4, criticals and specials recomputed with it), the full arc runs 0 → 2 →
4 → eased to 2 → pushed to 5 → breaks → 3 with no errors, the sheet shows the
band, and the rules journal has a Panic page generated from the same tables.
Version 1.1.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
ab619f4b73 |
Remove the dice textures and the whole Dice So Nice path
Deleted: the seven PNGs in dice/, tools/make-dice-textures.mjs, the `npm run dice` script, the RB_DICE colourset and texture tables, diceSetFor, the diceSoNiceReady hook, and rbDressDice — which was tagging ten roll sites with coloursets that no longer existed. 99 lines of module, plus the assets. README and DEPLOY updated; they still advertised all of it. Recoverable with `git revert` if it is ever wanted back. This also explains the 01-that-was-12. The RESULT was never wrong: skillRoll rolls one 1d100 and grades on roll.total, and the chat card derives the tens and units it prints from that same total. Checked all 100 values of percentileFaces and 60 live rolls — the printed faces reconstruct the graded number every time, with no mismatches. What disagreed was the ANIMATION. The custom addDicePreset labels registered for d100 and d10 remapped which physical face means what, so Dice So Nice could land showing 01 while the roll behind it was 12. Nothing downstream ever read the animated faces, so no roll was ever adjudicated on the wrong number — but a table watching the dice had every reason to believe otherwise, which is its own kind of wrong. Disabling the skins in 1.0.6 already removed it; deleting them makes it permanent. Verified in a running Foundry after removal: skill roll, weapon damage, hit location, major wound and Coherence all resolve and post their cards, no errors, and game.ringbrp.diceSetFor is gone from the API. Version 1.0.7. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9b6c61ffcb |
Turn the custom Dice So Nice skins off
Registering them with "preferred" made them the default for everybody in the world, which overrides whatever dice a player had already chosen — the system helping itself to a setting that belongs to the user. Off at the author's request. One named constant rather than a settings API: this codebase registers no game settings at all, and adding that surface, its lang keys and its UI to switch one feature off is more machinery than the job needs. Flip RB_DICE_ENABLED to true and everything below it still works. The textures in dice/ and the generator behind `npm run dice` are left alone. Nothing reads them while this is false, and deleting work in order to turn a feature off is a poor trade. Verified in a running Foundry with Dice So Nice active: "custodians" is no longer among the registered systems, and preferredSystem has gone back to "standard" rather than ours. Version 1.0.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
51bd8e618a |
Audit every remaining sheet and dialog; add the Culland Hall scene
Opened all of it this time, rather than driving the system through its API.
Two more defects of the same family as the NPC sheet, and in the same place —
the types added last:
- Talent and Art had no TYPES.Item.* label, so their sheets printed the raw
key "TYPES.Item.talent" under the name.
- Neither had a branch in item-sheet.hbs or an isTalent/isPower flag, so the
Detail tab rendered EMPTY over a document holding category, rule, uses per
case, Coherence cost, push cost and origin. Eleven of thirteen types were
handled, which is exactly the shape an eye skips.
Both now have a header statline and a full Detail section, and a guard walks
template.json: every declared type must have a label, a flag set in ringbrp.mjs,
and an {{#if isX}} section in the sheet.
One hardening fix: loseCoherence applies the cost and THEN announces it, so
anything that throws in between leaves the actor quietly poorer with no message.
localize() throws on a non-string, which an unrecognised reason key is.
Everything else came back clean, and it is worth recording what was checked so
the next audit knows what "checked" meant: 13 item types opened, all render; the
expedition sheet (4 tabs); all 7 chat cards driven through their real entry
points — roll, coherence, reaction via Combat.rollAll, hit location, weapon
damage, major wound, and contact through a case file that actually has a group;
all 3 dialogs opened by clicking the controls a player clicks. No errors, no raw
localisation keys anywhere.
Also: Culland Hall as a playable Scene. Drawn as SVG rather than generated,
because a room count, a door position and a label are all load-bearing here and
a generator cannot be trusted with any of them — three label-collision passes in
the browser before it was clean, which is the documented cost of doing it this
way. 2800x2000 at 100px to the square so the Foundry grid lands on the grid
drawn into the image; 5ft squares; no walls, because half-done walls are worse
than none and a GM who wants vision can add them in ten minutes.
Verified in Foundry: the scene imports, activates, and draws.
Version 1.0.2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
7f8355f0d0 |
Add the player primer and cheat sheet, and brand every journal
Ten pages a player reads before session one, in their own compendium at OBSERVER ownership: nine of setting, one cheat sheet. The draft called the organisation "the Bureau"; everything else in the game calls it the department or the agency, so that is what it is called. Thin areas filled rather than padded: the seven character principles as a list, the real Coherence band table instead of a paragraph describing one, and "what continuity is not" pulled out of the middle of a page because it is the most important sentence for a new player. The cheat sheet's numbers are computed from rules.mjs and lang/en.json at build time. A hand-typed one would have been the sixth time this project shipped a number that disagreed with the code, and the one the players were holding. R-250: the design tokens were never on :root. Journals rendered as default parchment, and four things were wrong in sequence — v14's hook is renderJournalEntrySheet; its `element` is the node, so `[0]` returns the first CHILD and put the class on a header button; there is no .journal-page-content in v14; and then the real one: with the selector matching, font-size, line-height and padding all applied while every colour did not, because the palette is declared on .ringbrp-sheet/.ringbrp-chat/.ringbrp-window and never on :root. A journal inherited no tokens, so every var() silently became nothing. A rule that HALF applies is not a specificity problem — I spent three attempts treating it as one, including a version bump on a cache theory that was wrong. Branded: primer, rulebook, starter GM text and handouts. Scoped by flag, so other modules' journals are untouched. Version 0.9.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
43508259ac |
Consolidation pass after a Codex review
Four real defects, each verified against the source before acting on it. Trade expertise was coupled to agency rank. A probationary burglar came out at 35-50% and a veteran at 65-80%, as though fifteen years of picking locks were something the department conferred. TRADE_BANDS is now fixed at 50-65/30-50 at every rank, and INDUCTION scales with service instead (20-32 probationary to 45-60 veteran) because that half genuinely is the department's. The two axes were the wrong way round. This is the thing I flagged myself after PAPER HARBOUR and then left alone — flagging a defect is not the same as fixing it. Bonus points were sprayed across every skill in the game. grantFullSkillList runs immediately before the 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. The comment two hundred lines above has always said "over the core skills"; the code never did. "Random species" passed "baseline" into generators that support random perfectly well. No behavioural tests. The strongest point in the review: the three guards are static and none of them can tell whether a rule is READ, which is the only kind of defect this project has ever shipped. tools/check-behaviour.mjs now runs 28 deterministic tests, gated into the build, covering R-231/232/243/245/246, graded defences and the lamp rule. Proved it bites by re-breaking R-246. The register moved to postings.mjs, a Foundry-neutral module the engine, rulebook, guards and tests all import normally — they were previously pulled out of the engine with a regex and eval, which worked and was a trap. My first version of that used `export … from`, which re-exports without local bindings, so ROLES was undefined and the system threw on init: node --check passes that, loading it in Foundry does not. Also: the contact doc comment still said "only OFFER moves standing"; check-kits accepted mas/app, which are the DISPLAY names of siz/cha, so a posting declaring one would have passed and produced an undefined characteristic; package.json disagreed with the manifest; README counts were three revisions stale and are now generated at build. Deferred with reasons: splitting the 6,700-line engine into modules. Right, and a multi-session refactor whose risk is exactly the silent breakage demonstrated above. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c60485de72 |
Play PAPER HARBOUR with the criminal, and fix R-246
A pier that closed in 1974, a beacon still transmitting, and a probationer recruited out of a city with a caution the department has quietly lost. The generator's gap — somebody uses that agent's name before it is given — landed on him in phase 2, and he held it on Fast Talk 14 against 48 while the Field Lead missed the read. The case turned on a warrant card: the keepers of the ground want relieving by somebody official enough to take the duty on, and every agent carries exactly that. Bargain 27, favourable, rolled 33 — impression -1 to +1. The one unambiguously his moment was the log the first team locked in behind them: 11 against Sleight of Hand 50. 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. Impression now falls on a fumble at any stage and still only rises on an offer; the narrower fix matters because read's delta counts FACTS LEARNED, not standing, so applying the delta always would have paid the team impression for doing research. Also logged: a probationary criminal is out-sneaked by a career Field Lead (47 to 60) because trade skills use the agency tier bands — defensible, flagged, not changed. And one invented step of my own: I rolled Transposition to get them home after the extraction phase had already put them out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
79acc8227f |
Ten trades, an induction layer, and rolling the stats yourself
TRADES: ten ordinary jobs the department recruits from — construction worker, doctor, professor, engineer, law enforcement, IT specialist, scientist, linguist, entertainer, criminal. They are not postings. A posting is what the department made of you; a trade is what you were before any of it. A trade character is built in two halves. The trade is genuine expertise on the same bands a posting's skills use — a doctor is a proper doctor. 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. The band is low on purpose; six weeks is not a career, and that gap is most of what it feels like to play one. Induction never demotes: a former police officer keeps pistol 58 from the trade rather than being re-taught it at 26. The creation dialog gains a "Recruited as" switch with its own trade list and card (which shows both halves), and check-kits now holds trades and induction to the same standard as postings — both new guard paths verified by breaking them. Rolling them yourself: a tickbox that walks the eight characteristics one at a time, each roll announced to chat so the dice actually fall, "Take them" locked until all eight are down. A hand-rolled character is taken exactly as it fell — no best-of-three on the key stat, no species shift. Verified both ways. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
53eb72c100 |
Issue riot shields to Containment and the Night Warden
Both postings draw a riot shield, and both are trained to use it — issuing one 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. melee_weapon:shield is SWAPPED into each support list rather than added, so five core and five support are preserved and the shield arrives without a free power bump. Containment drops stealth (a posting whose job is being the thing in the doorway was always the least stealthy in the department); the Night Warden drops navigate (they sit with one site all night). check-kits now requires the Shield skill for an issued shield, the same way it has always required training for an issued weapon. Verified by removing the skill and watching the build refuse. Four generated agents: shield skill at 25-30%, nobody encumbered, though a Containment officer lands at 32.9 of 33 kg. The shield ships slung — raising it is the choice, and for the Warden it is the difference between 2 points of armour and 5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c61700566c |
Shields: armour you raise, and the only armour you can defend with
A shield is the one piece of armour you hold, and it protects you exactly as much as you are currently holding it up. Raised, its points apply to what it covers and it can parry on a new Shield skill. Lowered, it is weight. The price of keeping it up is that it occupies a hand and you are shooting around it: -20% to your own ranged attacks, printed on the card like everything else. Only one shield goes up at a time. Seven of them, 2-point buckler to 6-point ballistic shield; the pavise and barrier plate cover legs too 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. A blow costs the shield a point of wear for every step it stood above an ordinary success, so an ordinary hit costs nothing, a special 1 and a critical 2. At its limit it comes apart, drops, and will not be raised again. Two defects found by driving the real functions rather than reading the diff, both introduced by this change: - Every shield defence quietly rolled DODGE. "shield" passed through defenceTypeAllowed unchanged, so neither parry branch matched and it fell through. Invisible in the card; it showed up as eight rolls at a supposed 85% returning six failures. Taking a blow on the shield IS a parry, so it is now a flag on a parry rather than a defence type of its own. - The wear rule was unreachable: it fired only on a clean stop, but under graded defences only a critical fully stops a critical, so a shield would never have taken a scratch. Wear is for what the shield ABSORBED — dragging a critical down to a special is exactly when shields splinter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6ed9ded362 |
Armour covers what it covers, and Coherence moves to the vitals
Asking for the type of armour per 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 containing sallets, lobster-tail helmets and three cuirasses, that could not be printed 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 wears it: all, torso, torsoArms, torsoLimbs, head. The sets expand to location KINDS, so a cuirass covers a baseline torso and a vesh trunk and ridge without either knowing the other exists. Unstated coverage is `all`, so anything that does not declare behaves 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 the global worn total PLUS the body's own armour at that location, which was right while armour was global and becomes a double count the moment a location knows its own pieces. It reads the location's total and nothing else, verified by firing fourteen hits of 10 damage across every location and asserting damage applied == 10 - that location's armour. A single ARMOUR number no longer exists, so the header shows the torso with "0-8 by location" beside it when the rest of you is not that. This is a real difficulty change: limbs are frequently bare and limbs are 12 of 20 on the melee table. Coherence moves out of the derived strip into the vitals at hit-point size, with its band under it and the box reddening as it falls. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3421ed4a2e |
Reorder the combat tab, and make weapons readyable
There was no way to ready a weapon. The strip under the characteristics reads `equipped`, and the only toggle-equipped control on the sheet was on the ARMOUR list — a weapon could only be readied by opening its item sheet and finding the tickbox there. The weapons table now carries the same toggle, the row is tinted when readied, and a line above the table says what the control does. The tab runs in the order it is used: weapons, armour, defend, reaction, body last. Weapons lead with 40px art and a display-face name, having previously sat below two screens of anatomy and reaction arithmetic. The figure is a partial, drawn in three places — header thumbnail, combat tab, anatomy tab. Three inline copies 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. The header carries a 34px figure and every location with its damage and the armour over it, so "how much armour is on the part that just got hit" is answered on the main page without changing tab. Also fixed: `.ringbrp-sheet a` outranks a bare `.rb-equip`, so an unequipped toggle was painted white and read as lit — true of the armour toggle too, since it was added. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4bfa7110d0 |
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%. A lamp now has a reach, per item rather than global, because a weapon lamp and a vehicle searchlight are not the same offer — 4 m for cold light strips, 6 m for a weapon lamp, 10 m for the issue field lamp, 30 m for a roof searchlight. Anyone inside it is in the light and the card names whose lamp it is, so both halves of the rule travel together: the pool is one position and the whole group is standing in it. Measured from the tokens. The shot that started this — no lamp of her own, four metres from somebody else's, in pitch dark — goes from 20% to 60%, while an agent twenty-five metres out still eats the full -50%. An UNKNOWN distance is explicitly not lit: Number(null) is 0, so an unmeasured distance would otherwise have read as standing on top of the lamp and lit any scene with no tokens placed. The dialog carries a tick instead, pre-set when the canvas could measure it and the GM's answer when it could not. Note: template.json is read when Foundry loads the system, so a deployed server needs a restart and not just a pull before the migration can stamp the new field. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5c640d65cf |
Fix a fast action firing twice and paying once
Ammunition was charged once before the shot loop rather than once per shot, so the Webley put two rounds out and the cylinder went down by one. That is the exact dishonesty the burst rules exist to avoid, and it landed on the mode that is supposed to cost the most. Charged per shot now, and the loop stops when the weapon runs dry part way through the action. Found by playing the four modes side by side in the goods yard — one agent per mode, all four at 60%, so the fight read the mode and not the shooter. Full log in docs/REVIEW_LOG.md, including two findings left open: burst options are close to unselectable once the light is bad, and a lamp currently lights only its carrier while three agents stand in the same pool of it at -50%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2e10ecced9 |
Fire modes, fitted sights and lamps, and a tab rail
Selectable fire, with the modifiers on the choice Every weapon has carried a fireMode since the packs were first built and nothing read it: a musket could rip a ten-round burst and "rapid" bought a weapon nothing. FIRE_MODE now gates the burst rules and carries its own attack modifier — slow +10% and then you work for the next one, fast a second shot at -20% to both, rapid bursts, beam +5% and it can be held on the target for damage instead of hits. The gate lives in burstAttack too, so no macro can route round it. `fast` was on five catalogue weapons and was not in the system's list of modes at all, so re-saving one of them silently blanked it; the list is derived from the rule now. burstAttack shipped complete and was reachable from nothing. It is the resolution path the selector chooses, and takes the dialog's whole stack rather than charging the player twice for the same burst. Sights and lamps that apply themselves A sight 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 band is already known when the modifier is wanted, so nothing is selected and nothing is forgotten. The dark costs something now; a carried lamp buys it back to dim, never to daylight, and the card says it shows them exactly where you are. A migration fits gear that predates the fields, matched on the stamped key rather than by name. The damage card The last card still wearing Foundry's stock dice styling — it posted a bare Roll and let the core template dress it. It renders through the module's own card now, with the Roll still on the message so dice stay inspectable. A rail instead of a strip Eight tabs across an 820px sheet compressed to 11px type. They run down the side now, one line each, with counts as badges; the sheet is 152px wider to pay for it. Also fixed: any weapon with a Coherence cost could not be attacked with AT ALL — the handler re-read the weapon after an await, when currentTarget is already null, and the attack died on a TypeError with nothing shown. And the duplicate-rule guard failed an honest table because FIRE_MODE and speedBands share two key names; it wants a majority now, and prints partial overlaps rather than swallowing them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ee9e1f5b73 |
Codex review pass: three real faults corrected
R-228: Ambidextrous was a compulsory build. 0/-20 gave a full-strength main hand AND a cheap second attack. It now buys consistency, not a free attack: -20 in both hands against -20/-40 untrained. R-229: full auto was a firing squad. Ten independent hits off one critical, each with its own damage, location and Major Wound check. Hits capped at 3/5/7, and a burst is ONE wounding event however many rounds connect. R-230: Engines deleted the pressure it existed to create. Guns +2 and Helm +1 against a vent of 3 made a fighting round heat-neutral, so Heat only mattered when the engineer failed. Venting is now 2/3/4. Also: falling halving no longer stacks (soft + controlled quartered the drop); blast has one formula floored once, so the answer no longer depends on call order. And the structural note — "giving everyone a roll is not the same as giving everyone a decision". Sensors, Damage Control and Medical now have mutually exclusive modes that bite in the engine: mark/spoof/read, patch/brace, restore/steady. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
226f853ee6 |
Hazards, automatic fire, two weapons, a bestiary, and space combat
Stress test first: 150 characters across every posting, tier and species against fourteen invariants each, then the edges (characteristics at 1/3/21, every location destroyed, twelve pikes carried, hp set to -99). 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. destructionOutcomeFor() now says what destruction means. THE WORLD: falling (a die per 3 m, armour does not apply), drowning (CON rounds then a point a round, straight past the Major Wound threshold), fire (a condition with a size; armour helps on the round you catch and never after), explosives (one roll at the centre, shared by band, cover beats armour). AUTOMATIC FIRE: one roll, the success level says how much of the burst arrived, and the rounds are spent either way. TWO WEAPONS: -20/-40, or 0/-20 with the new Ambidextrous talent; the off hand takes the lighter weapon and nothing over 2 kg. BESTIARY: 21 entries in three families — folklore, horror, and what comes back from further along — each with characteristics, natural armour, tactics and a POWER written as a rule with a cost. SPACE: eight stations, each on a skill somebody already has, so the Physician has a chair. Range is a band; the helm moves one a round, in or out. Heat is the pressure and took two rounds of playtest tuning to stop being a countdown. 64 rules, 198 spot-checked formulas, a 12-page rulebook in reading order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
79caf143bd |
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 base of 5 — technically trained — and led every phase ahead of a Rigging Officer on 53% Navigate. Tradecraft still wins ties and wins when genuinely better; it no longer wins when plainly worse. The Containment phase went 6% -> 26% on the same team. Found playing GLASS LANTERN with the historical gear: salvaged polearms and slings correctly arrive at 5% because nobody trains them, and maille over brigandine (AR 8) reduced four utility-knife hits to zero damage each. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e2d0ed5bb2 |
A player briefing, and the historical range
The generator was already writing the timeline, the cast and the texture and filing all of it behind the GM screen. New PLAYERS page: a read-aloud opening, what it is like there, what is known so far, who is there and what they want — but never what they lie about. Page ownership is now declared by the page rather than guessed from its index, which would have hidden any second player page. Historical range: 38 weapons (from 6) and 14 armours (from 2), each with its period stated, from arming swords and longbows through matchlocks and Brown Bess to the Lee-Enfield and a trench club. Five new skills to hang them on, including Firearm (Muzzle-loader), because a muzzle-loader is not a rifle. 275 icons; every weapon maps to a real skill and every item has art. Catalogue is 206 items across four eras. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
22a5067153 |
R-225: the extraction spends the way home, not hit points
Every case carries a "window closes" clock whose stated cost is THE WAY HOME and whose steps are "the margin narrows / the return is Difficult / the return is aimed at -30%". Nothing ever read it, so the phase whose job is getting out had no consequence of its own and the GM had to invent one. Extraction now advances that clock: failure and fumble narrow the window, a critical opens it back up, and returnCrossing() applies the penalty alongside the classification modifier and the Coherence band. 0/-10/-20/-30, Difficult from two steps. Also: legs now store their `kind`. The extraction check matched on the terrain prose and worked only because the string begins "Extraction —". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5c114601a9 |
R-224: a body at zero absorbed damage for free
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 and the system would not notice. Damage at zero now breaks the stabilisation and costs a round of the count, and hitLocationRoll calls died() when that meets the limit. Found by playing LOW VERGE until it killed somebody: two agents who were medically stable died in the water on a failed tidal extraction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
89ce557355 |
Death at zero, and a case brief that answers the useful questions
R-223: nothing happened at 0 hit points. An agent was marked down and lay there forever; the only mention of death in the system was flavour text on a destroyed head. Zero now starts a clock whose length is the agent's own Major Wound threshold, ticked from the same hook that charges blood loss. First Aid or Medicine stabilises and stops the count; healing above zero clears it; heal() refuses the dead. On death, Coherence decides what the record says — from a proper death in service down to not being recorded as dead at all. Case generator: four phases instead of five (the arc drew two interchangeable middles that read as padding). The brief now leads with Where / When / Classification / Phases, states the opposition in full, and has a "How you get back" section. place and duration were generated and never printed. The same facts are on the case file itself. 300 seeds checked for object leaks; one found and fixed (group taboo/desire are objects with .text). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1b44fccb79 |
Retune the case ramp, and let leading a phase earn experience
The ramp made every phase after the first Difficult (halving the lead's skill) and stacked a cumulative -5% on top. Separated in phaseRampFor() in rules.mjs: difficulty belongs to the phase (only Containment is Difficult, and it does not also ramp), the modifier is attrition (-5 a phase, capped at -15). lead 55% was 55 -> 22 -> 17 -> 12 -> 7 now 55 -> 50 -> 45 -> 27 -> 40 Over 20,000 simulated cases with destitution modelled, phases passed roughly double and destitution incidence roughly halves. R-222: resolveLegDialog passed no skillFamilyId, so the choke point could not resolve the skill item and leading a phase never earned a tick — Tradecraft, the skill the job most uses, could never improve. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1a0bdbd2df |
R-221: the phase-lead fallback could never fire
bestIn("tradecraft") ?? bestIn("navigate") never reached Navigate, because every
character carries every skill and skillRating() returns the printed base rather
than null — so the first lookup always found somebody. A team of postings that
train Navigate and not Tradecraft was led by whoever had the highest UNTRAINED
Tradecraft, which is 5%, putting phase three at 1%.
TRAINED now means above the printed base. Measured on a Pathology / Overwatch /
Warden / Materials team: lead 5% -> 34%.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
1734612c06 |
v0.7.0 — vehicles, Borrowed Authority, and a door for the case generator
Since 0.6.1: - VEHICLES as their own Item type: crew, seats, cargo, hull, armour, range, terrain and the skill that works them, with speed as a band. 14 of them, from the pool car and the Custodian van to a sealed crawler and the carriage that is already parked outside wherever you were going to go. - STORES roughly doubled to 118 gear items across four eras — standing issue and paperwork, transport stores, and the forward stores Registry will not hand a probationary officer. Catalogue now 175 items, 231 icons. - Agents leave creation properly equipped: standing issue, the posting's kit, what seniority adds, and one personal effect that is theirs and not the department's. - All 25 postings now describe themselves in the creation dialog. - BORROWED AUTHORITY, the access subsystem. No hacking skill and nothing to break into: access is a named credential with a scope, a Trust that erodes, and red flags describing how its real owner behaves. Inside scope there is no roll. Two tiers beyond is impossible at any skill. Coherence widens a record, and pushing one past what its Trust will carry merges your file with theirs. - Defences graded by level, and damage now rolls at the level a blow landed at. - Dodge's DEX x 2 base is finally evaluated; it read 0% for most postings. - Brawl is a fallback rather than a peer of the weapon in hand. - check-lang guard: one key that was both a value and a branch silently un-localised the entire system. - The case generator is reachable from the Actor Directory, and no longer puts every character in the world on a new case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d0144f744e |
The Custodians v0.6.1 — full system
A Foundry VTT game system: Basic Roleplaying d100 in a drab British agency that deals with crossings, where reality keeps a record of what belongs where and can be made to get it wrong. Contents: - rules.mjs as the single authority for every rule formula, with 40 rules and 132 spot-checked values. Nothing may restate a formula it defines. - 25 postings, each with a key characteristic, trained skills, talents and its own kit; a 175-item catalogue across four eras; 231 generated icons. - Reaction Lanes initiative, re-rolled every round including the first. - Coherence, and the Quiet Arts, which only ever arrive from a crossing gone wrong. - Graded defences: only a critical stops a critical. - Borrowed Authority — access as a record rather than a hacking roll. - A case generator, and a rules journal generated from rules.mjs so the book cannot drift from the code. Built compendia are committed so a remote Foundry needs only the files. node_modules is not; `npm install` restores it for rebuilding content. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |