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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c0d33ee81d
commit
f847cb2171
@@ -183,7 +183,7 @@ The build runs all nine guards before it writes anything:
|
||||
```
|
||||
check-rules: OK — 108 rules, 34 files scanned, 3 aliases + 46 constant sets checked, 387 formulas verified
|
||||
check-kits: OK — 25 roles, 10 trades, 218 catalogue items, every kit key resolves, every posting can use what it carries, every loadout distinct
|
||||
check-lang: OK — en.json, 1016 keys, no leaf/branch collisions
|
||||
check-lang: OK — en.json, 1018 keys, no leaf/branch collisions
|
||||
check-templates: OK — 19 templates compile
|
||||
check-behaviour: OK — 76 behavioural tests
|
||||
check-scenarios: OK — 10 scenario files, 78 mechanic tags, every skill named resolves against 62 catalogue entries
|
||||
|
||||
@@ -5363,3 +5363,35 @@ generated and validated against `npm run check`, and three sentences around it s
|
||||
"the eight guards" while nine ran. `update-readme` now rewrites the count word as well as
|
||||
the block. The comment warning that a hardcoded list goes stale was sitting four lines
|
||||
above the prose that had gone stale.
|
||||
|
||||
## R-256 — the header said 9 while the body said 4
|
||||
|
||||
Verifying R-255 on a live sheet showed the rule working everywhere it was built and
|
||||
contradicted in the one place a GM actually looks first. The header's ARMOUR field is the
|
||||
hide the creature HAS; grounded, the hit-location table underneath it read 4 and the
|
||||
header still read 9. Nothing computes off that field, so nothing was wrong in the code —
|
||||
it was wrong at the table, which is the only place that counts.
|
||||
|
||||
The field stays editable and stays 9, because 9 is the creature's hide and that is what a
|
||||
GM needs to type into. Underneath it, when `system.grounded` is set, the sheet now states
|
||||
what is in play: **DOWN: 4 · CHEST 9**, from `exposedHideFor` rather than from a second
|
||||
opinion, with the reason on the tooltip.
|
||||
|
||||
**The guard, and what it caught on the way.** check-rules forbids redefining a rule BY
|
||||
NAME, which misses how this actually goes wrong: nobody redefines `exposedHideFor`, they
|
||||
write `Math.floor(hide / 2)` in the sheet because it is one keystroke cheaper than an
|
||||
import. I wrote the wrong version first to prove the point and check-rules passed it
|
||||
happily. It now fails any file outside `rules.mjs` that halves a hide inline, with the
|
||||
file and line; the check files are exempt, because independently stating the expected
|
||||
value is what a test is for.
|
||||
|
||||
**Open, and worse than what I fixed.** Writing that guard turned up two copies of a
|
||||
different armour rule. `armourAgainst` — critical ignores armour, special halves it —
|
||||
lives in `ringbrp.mjs:414`, not in `rules.mjs`, so the authority never covered it; and
|
||||
`tools/simulate.mjs:267` has its own inline version rather than importing it. They agree
|
||||
today, so no published number is wrong. But every figure in BESTIARY.md and the whole
|
||||
lethality baseline comes out of the simulator, so the day someone changes how a special
|
||||
hit meets armour, the game changes and the measurements do not, and the guards will all
|
||||
still pass. That is R-251 with a longer fuse. Not fixed here: moving a rule into
|
||||
`rules.mjs` and re-pointing the simulator at it is a change to the measurement pipeline
|
||||
and wants its own before-and-after.
|
||||
|
||||
@@ -201,6 +201,8 @@
|
||||
"RINGBRP.Sheet.MajorWound": "Major wound",
|
||||
"RINGBRP.Sheet.MajorWoundHint": "A single blow dealing this much, after armour, causes a Major Wound.",
|
||||
"RINGBRP.Sheet.Armour": "Armour",
|
||||
"RINGBRP.Sheet.GroundedHide": "down: {body} · chest {vital}",
|
||||
"RINGBRP.Sheet.GroundedHideHint": "It is on the ground. Half the hide is no longer between you and it — except the chest, which faces you whatever posture it is in.",
|
||||
"RINGBRP.Sheet.DamageMod": "Damage mod",
|
||||
"RINGBRP.Sheet.MOV": "MOV",
|
||||
"RINGBRP.Sheet.DexRank": "DEX rank",
|
||||
|
||||
+12
@@ -4713,6 +4713,18 @@ class RingBRPCharacterSheet extends ActorSheet {
|
||||
ctx.RINGBRP = RINGBRP;
|
||||
ctx.isCharacter = this.actor.type === "character";
|
||||
|
||||
/* R-255. The header field is the hide the beast HAS. Grounded, that is not the hide
|
||||
that applies, and a GM reading 9 off the top of the sheet while the body reads 4
|
||||
will use the wrong number — the same defect as a stat block that disagrees with
|
||||
its actor, one layer in. Say what is in play, from the function the body uses. */
|
||||
{
|
||||
const hide = Number(sys.naturalArmour?.value) || 0;
|
||||
const body = exposedHideFor(hide, sys.grounded, "leg");
|
||||
ctx.groundedHide = (sys.grounded && hide && body !== hide)
|
||||
? { body, vital: exposedHideFor(hide, sys.grounded, "vital") }
|
||||
: null;
|
||||
}
|
||||
|
||||
ctx.skillsByCategory = {};
|
||||
for (const key of Object.keys(RINGBRP.categories)) ctx.skillsByCategory[key] = [];
|
||||
for (const it of this.actor.items) {
|
||||
|
||||
@@ -228,6 +228,12 @@
|
||||
width: 44px; text-align: center; font-size: 24px;
|
||||
background: transparent; border: none; padding: 0; color: var(--rb-white);
|
||||
}
|
||||
.rb-grounded-hide {
|
||||
display: block; margin-top: 2px;
|
||||
font-family: var(--rb-body); font-size: 9px; font-weight: 700;
|
||||
letter-spacing: .04em; text-transform: uppercase;
|
||||
color: var(--rb-danger, #d9534f); white-space: nowrap;
|
||||
}
|
||||
.rb-wounded { border-color: var(--rb-danger) !important; }
|
||||
.rb-wound-flag {
|
||||
font-family: var(--rb-body); font-size: 10px; font-weight: 700;
|
||||
|
||||
+1
-1
@@ -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.10",
|
||||
"version": "1.7.11",
|
||||
"compatibility": {
|
||||
"minimum": 13,
|
||||
"verified": "14.364"
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
<input type="number" name="system.hp.value" value="{{sys.hp.value}}"/><span class="rb-slash">/</span><span class="rb-max">{{sys.hp.max}}</span></div>
|
||||
<div class="rb-mw"><span class="rb-vital-label">{{localize "RINGBRP.Sheet.MajorWound"}}</span><span class="rb-big">{{sys.majorWound.value}}</span></div>
|
||||
<div class="rb-armour"><span class="rb-vital-label">{{localize "RINGBRP.Sheet.Armour"}}</span>
|
||||
<input type="number" name="system.naturalArmour.value" value="{{sys.naturalArmour.value}}"/></div>
|
||||
<input type="number" name="system.naturalArmour.value" value="{{sys.naturalArmour.value}}"/>
|
||||
{{#if groundedHide}}<span class="rb-grounded-hide"
|
||||
data-tooltip="{{localize 'RINGBRP.Sheet.GroundedHideHint'}}">{{localize "RINGBRP.Sheet.GroundedHide" body=groundedHide.body vital=groundedHide.vital}}</span>{{/if}}</div>
|
||||
{{!-- The body plan. Every NPC has always HAD hit locations; nothing on this
|
||||
sheet ever let a GM choose which body they belonged to, so every bear and
|
||||
every dragon in the game was quietly a man with two arms. --}}
|
||||
|
||||
@@ -42,6 +42,25 @@ for (const rel of files) {
|
||||
}
|
||||
}
|
||||
|
||||
/* 1b. and none of them may re-derive the hide ------------------------------- *
|
||||
* Redefinition is caught by NAME above, which misses the way this actually goes
|
||||
* wrong: the sheet header wanted the grounded hide and `Math.floor(hide / 2)` is
|
||||
* one keystroke cheaper than importing exposedHideFor. Then the header and the hit
|
||||
* locations are two rules that happen to agree, until one of them is changed. The
|
||||
* exemption is the check files, where stating the expected value independently is
|
||||
* the entire point of a test. */
|
||||
{
|
||||
const inlineHalving = /Math\.(?:floor|round|ceil)\(\s*[^()]*\b(?:hide|naturalArmour)\b[^()]*\/\s*2/i;
|
||||
for (const rel of files) {
|
||||
if (rel.startsWith("tools/check-")) continue;
|
||||
const src = await readFile(new URL(rel, root), "utf8");
|
||||
src.split("\n").forEach((line, i) => {
|
||||
if (inlineHalving.test(line))
|
||||
fail(`${rel}:${i + 1} halves natural armour inline — exposedHideFor() owns that`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* 2. aliases must be identity-equal ---------------------------------------- *
|
||||
* REQUIRED is authoritative and hand-held on purpose. Discovery alone is not
|
||||
* enough: the original bug replaced an alias with an arrow function, which a
|
||||
|
||||
Reference in New Issue
Block a user