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>
This commit is contained in:
slaguru666
2026-09-12 22:57:31 +01:00
co-authored by Claude Opus 5
parent 97fe2704e8
commit ca040b7926
7 changed files with 141 additions and 7 deletions
+59
View File
@@ -5253,3 +5253,62 @@ 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.
+9 -5
View File
@@ -25,7 +25,7 @@ import {
reactionLoadFrom, reactionPrintedFrom, woundEffectsFor,
BLEED, bleedStoppedBy, bleedRateFor, locationEffectsFor,
locationFor, resolveLocationHit, locationsFor, woundPenaltyFrom, remapLocationDamage,
groundedPlanFor, naturalArmourOn,
groundedPlanFor, naturalArmourOn, exposedHideFor,
locationDamageReplacement,
encumbrancePenaltyFor, ENCUMBERED_CATEGORIES, freeCarryFor,
defencePenaltyFor, defenceTypeAllowed, DEFENCE_STEP,
@@ -578,7 +578,8 @@ class RingBRPActor extends Actor {
// --- Armour: what is worn, and what each piece actually reaches ---
// Every piece used to add to every location, so a Brodie helmet protected your
// legs. Pieces now declare coverage and the total is computed per location.
const naturalArmour = Number(sys.naturalArmour?.value) || 0;
// Down on its belly, half the hide is no longer between you and the animal.
const naturalArmour = exposedHideFor(sys.naturalArmour?.value, sys.grounded);
// A shield is armour you HOLD: its points count only while it is raised and only
// while it is still a shield. Lowered or broken it is weight and nothing else.
const wornArmour = this.items.filter(i => i.type === "armour" && i.system.equipped)
@@ -1111,7 +1112,7 @@ export async function hitLocationRoll({ target, damage, mode = "ranged",
const locArmour = live
? Number(live.armour) || 0
: locationArmourFor((Number(loc.armour) || 0)
+ naturalArmourOn(loc, sys.naturalArmour?.value),
+ naturalArmourOn(loc, exposedHideFor(sys.naturalArmour?.value, sys.grounded)),
sys.wornArmour ?? [], loc.kind);
effective = Math.max(0, effective - armourAgainst(level, locArmour));
}
@@ -1201,7 +1202,10 @@ export async function hitLocationRoll({ target, damage, mode = "ranged",
const wings = (target.system.locations ?? []).filter(l => l.kind === "wing").map(l => l.id);
const drop = Object.fromEntries(wings.map(id => [`system.locationDamage.-=${id}`, null]));
if (Object.keys(drop).length) await target.update(drop);
await target.update({ "system.speciesProfile": grounded });
// The flag, not the wing wounds, is what says it is down: once the wings leave
// its table there is nothing left to read them off, and being grounded lasts the
// rest of the scene rather than as long as a location does.
await target.update({ "system.speciesProfile": grounded, "system.grounded": true });
await ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor: target }),
content: `<div class="ringbrp-chat rb-grounded"><strong>${
@@ -6324,7 +6328,7 @@ Hooks.once("init", () => {
npcTarget, npcShouldWithdraw, declareNpcTarget,
setReaction,
locationFor, locationMaxHp, resolveLocationHit, hitLocationRoll, locationModeFor,
remapLocationDamage, locationDamageReplacement, groundedPlanFor
remapLocationDamage, locationDamageReplacement, groundedPlanFor, exposedHideFor
};
});
+21
View File
@@ -597,6 +597,27 @@ export function groundedPlanFor(speciesId = "baseline") {
* wearing. WORN armour still covers it, so barding a wing works exactly as before;
* it is the animal's own hide that stops at the edge of the wing.
*/
/**
* The hide a creature still has between you and it once it is on the ground.
*
* Built to fix a defect in the two commits before it. R-95 made losing a wing change
* the animal and R-96 made the wing hittable by taking the hide off the membrane —
* and together they handed the party a soft target whose destruction REMOVES the soft
* target. Measured: six of twenty ranged faces land on a bare wing while it flies and
* none do once it is grounded, so putting it on the ground took the party's chance of
* winning from 44.9% to 1.4%. Grounding it was a 43-point mistake, and the creature's
* own tactics called it "the fight the agents can actually win".
*
* A heraldic beast is armoured the way a heraldic beast is drawn: across the back and
* the flanks. Down on its belly it is not, so half the hide stops being between you and
* it. That restores something an ordinary round can get through, which is the thing
* grounding took away, and it does it without pretending a grounded animal is harmless.
*/
export function exposedHideFor(naturalArmour = 0, grounded = false) {
const a = Number(naturalArmour) || 0;
return grounded ? Math.floor(a / 2) : a;
}
export function naturalArmourOn(location, naturalArmour = 0) {
return location?.bare ? 0 : (Number(naturalArmour) || 0);
}
+1 -1
View File
@@ -2,7 +2,7 @@
"id": "ringbrp",
"title": "The Custodians",
"description": "A Basic Roleplaying game of agents who cross the crossing to contain what should not be there — and who come back a little less from here each time.",
"version": "1.7.8",
"version": "1.7.9",
"compatibility": {
"minimum": 13,
"verified": "14.364"
+1
View File
@@ -107,6 +107,7 @@
"naturalArmour": {
"value": 0
},
"grounded": false,
"tactics": "",
"dying": {
"rounds": 0,
+44 -1
View File
@@ -30,7 +30,7 @@ import { fileURLToPath } from "node:url";
import { LOCATION_TABLES, BODY_ART } from "../anatomy.mjs";
import { locationEffectsFor, destructionOutcomeFor, remapLocationDamage,
locationDamageReplacement, locationMaxHp, groundedPlanFor,
naturalArmourOn } from "../rules.mjs";
naturalArmourOn, exposedHideFor } from "../rules.mjs";
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
const problems = [];
@@ -270,6 +270,49 @@ for (const [id, table] of Object.entries(LOCATION_TABLES)) {
}
}
/* 6. coming down must not be a reward for the creature ----------------------- *
* The defect this exists to catch shipped twice before anyone measured it. R-95 made a
* lost wing change the animal; R-96 took the hide off the wing so the wing could be lost
* at all. Together they gave the party a soft target and then deleted it the moment the
* party hit it: six of twenty ranged faces land on a bare wing while it flies and none
* do once it is grounded, so the creature's own "disable either wing and it is the
* fight you can actually win" was worth minus forty-three points of win rate.
*
* So: measure what an average round of ordinary fire gets through, before and after.
* A flyer that is HARDER to hurt on the ground than in the air fails, whatever the
* fiction says about it. */
{
/* Averaged across the whole range a round in this game can do — 1d3 to 1d10+2, before
and after the borough halves it — because the answer genuinely changes with the size
of the round and a single probe picks its own conclusion. A 4-point round does more
through a bare wing than through a halved hide; a 9-point round does far less. The
first version of this check measured only at 4 and duly reported that the fix had
failed. Small arms prefer it airborne; anything heavier wants it down. */
const HIDE = 7, RANGE = 12;
const through = (planId, grounded) => {
const t = LOCATION_TABLES[planId];
let total = 0;
for (let shot = 1; shot <= RANGE; shot++) {
for (let face = 1; face <= 20; face++) {
const loc = t.locations.find(l => face >= l.ranged[0] && face <= l.ranged[1]);
if (!loc) continue;
total += Math.max(0, shot - naturalArmourOn(loc, exposedHideFor(HIDE, grounded)));
}
}
return total / (20 * RANGE);
};
for (const [id] of Object.entries(LOCATION_TABLES)) {
const landing = groundedPlanFor(id);
if (!landing || !LOCATION_TABLES[landing]) continue;
const air = through(id, false), ground = through(landing, true);
if (ground < air) {
problems.push(`${id}: an average round gets ${air.toFixed(2)} through it flying and only `
+ `${ground.toFixed(2)} once grounded — putting it on the ground would make it HARDER `
+ `to kill, and its own tactics tell the GM the opposite`);
}
}
}
if (problems.length) {
console.error("check-anatomy: FAILED");
for (const p of [...new Set(problems)]) console.error(" " + p);
+6
View File
@@ -548,6 +548,12 @@ const checks = [
RULES.naturalArmourOn({ kind: "wing" }, 7), 7],
["naturalArmourOn(no location at all) keeps the hide",
RULES.naturalArmourOn(null, 7), 7],
// On its belly, half the hide is no longer in the way — which is what gives an
// ordinary round somewhere to land once grounding takes the bare wings off the table.
["exposedHideFor(flying) is the whole hide", RULES.exposedHideFor(7, false), 7],
["exposedHideFor(grounded) is half of it", RULES.exposedHideFor(7, true), 3],
["exposedHideFor(grounded, thin hide)", RULES.exposedHideFor(1, true), 0],
["exposedHideFor(no hide at all)", RULES.exposedHideFor(0, true), 0],
// 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 this is where a wound
// was silently thrown away instead of carried.