Files
slaguru666andClaude Opus 5 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 ddc4f99 and shifted the shared random stream
for every creature measured after it. Every drift sat under the guard's
six-point tolerance, so it passed while being wrong. Worth knowing that
the file's claim — "the only thing that can move the number is a change
to the rules or to the creature" — is not currently true: adding any
creature perturbs the rest. Seeding per creature would fix it and is
not done here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 22:16:54 +01:00

188 lines
12 KiB
JavaScript

/**
* Species anatomy: where a creature can be hit, and what it looks like.
*
* This lived inside ringbrp.mjs, where only Foundry could reach it. Node cannot import
* the engine — it touches `game`, `Hooks` and `foundry` at load — so any tool that wants
* to know a Cadence has five bodies and no head had no way to ask. tools/make-portraits
* and tools/mj-queue both need exactly that, for the same reason rules.mjs exists: art
* generated from the creature's own geometry CANNOT contradict its hit-location table,
* because they are the same numbers.
*
* Moved byte for byte. ringbrp.mjs now assigns RINGBRP.locationTables and RINGBRP.bodyArt
* from here, so the engine and the tools read one definition. There is no second copy.
*/
/* Location fractions are of TOTAL hit points. They were sized against the old
26-point pool; halving HP left a leg at 4 against a sidearm doing 4-6 through
armour, so 64% of hits crippled the limb they struck. Raised so a sidearm
cripples ~40% of the time and a rifle ~55%. */
export const LOCATION_TABLES = {
baseline: {
label: "RINGBRP.Species.Baseline",
locations: [
{ id: "legR", label: "RINGBRP.Loc.LegR", melee: [1, 4], ranged: [1, 3], frac: 0.42, kind: "leg" , slot: "lowerR" },
{ id: "legL", label: "RINGBRP.Loc.LegL", melee: [5, 8], ranged: [4, 6], frac: 0.42, kind: "leg" , slot: "lowerL" },
{ id: "abdomen", label: "RINGBRP.Loc.Abdomen", melee: [9, 11], ranged: [7, 10], frac: 0.42, kind: "body" , slot: "body" },
{ id: "chest", label: "RINGBRP.Loc.Chest", melee: [12, 12], ranged: [11, 15], frac: 0.5, kind: "vital", slot: "vital" },
{ id: "armR", label: "RINGBRP.Loc.ArmR", melee: [13, 15], ranged: [16, 17], frac: 0.35, kind: "arm" , slot: "upperR" },
{ id: "armL", label: "RINGBRP.Loc.ArmL", melee: [16, 18], ranged: [18, 19], frac: 0.35, kind: "arm" , slot: "upperL" },
{ id: "head", label: "RINGBRP.Loc.Head", melee: [19, 20], ranged: [20, 20], frac: 0.4, kind: "head" , slot: "head" }
]
},
/* A QUADRUPED — a bear, a horse, a hound, a big cat. Four legs, a barrel divided
into forequarters (heart and lungs, the killing shot) and hindquarters, a neck
worth aiming at, and a head that is HARDER to hit than a man's because it is
carried low and swung. The standard BRP animal table, split by attack type for
the same reason baseline is: a man with a spear is down among the legs, a man
with a rifle is looking at a broadside. */
quadruped: {
label: "RINGBRP.Species.Quadruped",
locations: [
{ id: "hindLegR", label: "RINGBRP.Loc.HindLegR", melee: [1, 3], ranged: [1, 2], frac: 0.42, kind: "leg" , slot: "lowerR" },
{ id: "hindLegL", label: "RINGBRP.Loc.HindLegL", melee: [4, 6], ranged: [3, 4], frac: 0.42, kind: "leg" , slot: "lowerL" },
{ id: "hindQtr", label: "RINGBRP.Loc.HindQtr", melee: [7, 9], ranged: [5, 8], frac: 0.42, kind: "body" , slot: "body" },
{ id: "foreQtr", label: "RINGBRP.Loc.ForeQtr", melee: [10, 12], ranged: [9, 13], frac: 0.5, kind: "vital", slot: "vital" },
{ id: "foreLegR", label: "RINGBRP.Loc.ForeLegR", melee: [13, 15], ranged: [14, 15], frac: 0.42, kind: "leg" , slot: "upperR" },
{ id: "foreLegL", label: "RINGBRP.Loc.ForeLegL", melee: [16, 18], ranged: [16, 17], frac: 0.42, kind: "leg" , slot: "upperL" },
{ id: "neck", label: "RINGBRP.Loc.Neck", melee: [19, 19], ranged: [18, 19], frac: 0.4, kind: "neck" , slot: "neck" },
{ id: "head", label: "RINGBRP.Loc.Head", melee: [20, 20], ranged: [20, 20], frac: 0.4, kind: "head" , slot: "head" }
]
},
/* A WINGED QUADRUPED — a dragon, a wyvern, a griffon. The quadruped, plus the two
things that make it a different fight: wings you can shoot down. On the ground
the wings are folded high and hard to reach; in the air they are the broadest
target on the animal, which is why the ranged column gives them six faces and
the melee column two. DISABLE ONE AND IT CANNOT FLY — see locationEffectsFor. */
winged: {
label: "RINGBRP.Species.Winged",
/* "a flyer with a hole in a wing is a quadruped" — the supporter's own tactics.
Disable either wing and the creature stops being a flyer: it drops to this plan,
the wings leave its hit table, and the six ranged faces they occupied go back to
the body it is now standing on. Declared here rather than in the engine so a
future flyer only has to say what it becomes. */
grounded: "quadruped",
locations: [
{ id: "hindLegR", label: "RINGBRP.Loc.HindLegR", melee: [1, 3], ranged: [1, 2], frac: 0.42, kind: "leg" , slot: "lowerR" },
{ id: "hindLegL", label: "RINGBRP.Loc.HindLegL", melee: [4, 6], ranged: [3, 4], frac: 0.42, kind: "leg" , slot: "lowerL" },
{ id: "hindQtr", label: "RINGBRP.Loc.HindQtr", melee: [7, 9], ranged: [5, 6], frac: 0.42, kind: "body" , slot: "body" },
{ id: "foreQtr", label: "RINGBRP.Loc.ForeQtr", melee: [10, 12], ranged: [7, 9], frac: 0.5, kind: "vital", slot: "vital" },
{ id: "foreLegR", label: "RINGBRP.Loc.ForeLegR", melee: [13, 14], ranged: [10, 10], frac: 0.42, kind: "leg" , slot: "upperR" },
{ id: "foreLegL", label: "RINGBRP.Loc.ForeLegL", melee: [15, 16], ranged: [11, 11], frac: 0.42, kind: "leg" , slot: "upperL" },
{ id: "wingR", label: "RINGBRP.Loc.WingR", melee: [17, 17], ranged: [12, 14], frac: 0.35, kind: "wing" , slot: "wingR", bare: true },
{ id: "wingL", label: "RINGBRP.Loc.WingL", melee: [18, 18], ranged: [15, 17], frac: 0.35, kind: "wing" , slot: "wingL", bare: true },
{ id: "neck", label: "RINGBRP.Loc.Neck", melee: [19, 19], ranged: [18, 19], frac: 0.4, kind: "neck" , slot: "neck" },
{ id: "head", label: "RINGBRP.Loc.Head", melee: [20, 20], ranged: [20, 20], frac: 0.4, kind: "head" , slot: "head" }
]
},
/* A VESH is bilaterally ASYMMETRIC: one long grasping limb, one short bracing
limb, and no head at all — the senses run along a dorsal ridge. There is no
single killing shot to the skull because there is no skull. The ridge is the
vital, and it is a long target rather than a small one. */
vesh: {
label: "RINGBRP.Species.Vesh",
locations: [
{ id: "footBrace", label: "RINGBRP.Loc.BraceFoot", melee: [1, 3], ranged: [1, 3], frac: 0.42, kind: "leg" , slot: "lower" },
{ id: "limbBrace", label: "RINGBRP.Loc.BraceLimb", melee: [4, 8], ranged: [4, 7], frac: 0.42, kind: "brace" , slot: "brace" },
{ id: "lowTrunk", label: "RINGBRP.Loc.LowerTrunk", melee: [9, 12], ranged: [8, 12], frac: 0.42, kind: "body" , slot: "body" },
{ id: "upTrunk", label: "RINGBRP.Loc.UpperTrunk", melee: [13, 15], ranged: [13, 15], frac: 0.42, kind: "body" , slot: "vital" },
{ id: "limbGrasp", label: "RINGBRP.Loc.GraspLimb", melee: [16, 18], ranged: [16, 17], frac: 0.35, kind: "grasp" , slot: "upper" },
{ id: "ridge", label: "RINGBRP.Loc.Ridge", melee: [19, 20], ranged: [18, 20], frac: 0.5, kind: "ridge" , slot: "head" }
]
},
/* A CADENCE is not one body. It is four to six small bodies that together are
one person, and the location roll picks which body you hit. Killing a body
does not kill the person — it reduces them. There is no vital and no head:
every body carries an equal share, and the person degrades as they are
whittled down. This is why shooting a Cadence feels like nothing is working. */
cadence: {
label: "RINGBRP.Species.Cadence",
locations: [
{ id: "bodyA", label: "RINGBRP.Loc.BodyA", melee: [1, 4], ranged: [1, 4], frac: 0.28, kind: "unit" , slot: "body" },
{ id: "bodyB", label: "RINGBRP.Loc.BodyB", melee: [5, 8], ranged: [5, 8], frac: 0.28, kind: "unit" , slot: "vital" },
{ id: "bodyC", label: "RINGBRP.Loc.BodyC", melee: [9, 12], ranged: [9, 12], frac: 0.28, kind: "unit" , slot: "head" },
{ id: "bodyD", label: "RINGBRP.Loc.BodyD", melee: [13, 16], ranged: [13, 16], frac: 0.28, kind: "unit" , slot: "lower" },
{ id: "bodyE", label: "RINGBRP.Loc.BodyE", melee: [17, 20], ranged: [17, 20], frac: 0.28, kind: "unit" , slot: "upper" }
]
}
};
/**
* The figure drawn on the combat tab. One entry per species profile, keyed by the
* same location ids as the table above, so a species that has a location gets a
* shape and one that does not simply has none — no separate list to keep in step.
*
* Deliberately blocky: these are readable at sheet size and honest about the
* anatomy. A Vesh is asymmetric with no head; a Cadence is five separate bodies.
*/
export const BODY_ART = {
baseline: {
viewBox: "0 0 120 196",
parts: {
head: { t: "ellipse", cx: 60, cy: 21, rx: 15, ry: 17 },
chest: { t: "rect", x: 40, y: 42, w: 40, h: 40, rx: 9 },
abdomen: { t: "rect", x: 44, y: 84, w: 32, h: 30, rx: 8 },
armR: { t: "rect", x: 18, y: 45, w: 17, h: 64, rx: 8 },
armL: { t: "rect", x: 85, y: 45, w: 17, h: 64, rx: 8 },
legR: { t: "rect", x: 43, y: 117, w: 15, h: 72, rx: 7 },
legL: { t: "rect", x: 62, y: 117, w: 15, h: 72, rx: 7 }
}
},
/* Side-on, because a quadruped seen from the front tells you nothing about which
end you hit. Landscape viewBox — the partial reads it from here, so a body plan
that is wider than it is tall simply draws that way. */
quadruped: {
viewBox: "0 0 208 132",
parts: {
hindQtr: { t: "rect", x: 34, y: 42, w: 58, h: 44, rx: 14 },
foreQtr: { t: "rect", x: 88, y: 38, w: 62, h: 50, rx: 14 },
neck: { t: "path", d: "M142 44 L176 24 L188 40 L154 62 Z" },
head: { t: "ellipse", cx: 186, cy: 28, rx: 19, ry: 14 },
hindLegR: { t: "rect", x: 40, y: 84, w: 13, h: 44, rx: 6 },
hindLegL: { t: "rect", x: 60, y: 84, w: 13, h: 44, rx: 6 },
foreLegR: { t: "rect", x: 104, y: 86, w: 13, h: 42, rx: 6 },
foreLegL: { t: "rect", x: 124, y: 86, w: 13, h: 42, rx: 6 }
}
},
/* The quadruped with both wings drawn spread rather than folded, because the
table's ranged column assumes a thing in the air. */
winged: {
viewBox: "0 0 208 132",
parts: {
wingR: { t: "path", d: "M96 40 L44 6 L18 26 L74 52 Z" },
wingL: { t: "path", d: "M100 44 L58 92 L82 116 L114 62 Z" },
hindQtr: { t: "rect", x: 34, y: 44, w: 56, h: 40, rx: 13 },
foreQtr: { t: "rect", x: 86, y: 40, w: 60, h: 46, rx: 13 },
neck: { t: "path", d: "M138 46 L172 26 L184 42 L150 64 Z" },
head: { t: "ellipse", cx: 184, cy: 30, rx: 18, ry: 13 },
hindLegR: { t: "rect", x: 42, y: 82, w: 12, h: 40, rx: 6 },
hindLegL: { t: "rect", x: 60, y: 82, w: 12, h: 40, rx: 6 },
foreLegR: { t: "rect", x: 102, y: 84, w: 12, h: 38, rx: 6 },
foreLegL: { t: "rect", x: 120, y: 84, w: 12, h: 38, rx: 6 }
}
},
/* One long grasping limb, one short bracing limb, a trunk that leans, and the
sensory ridge running down the back instead of a head. */
vesh: {
viewBox: "0 0 120 196",
parts: {
ridge: { t: "path", d: "M44 18 L70 14 L76 96 L52 100 Z" },
upTrunk: { t: "path", d: "M40 34 L70 30 L74 84 L44 88 Z" },
lowTrunk: { t: "path", d: "M44 90 L74 86 L78 132 L48 136 Z" },
limbGrasp: { t: "path", d: "M74 36 L96 44 L104 148 L86 150 L82 60 Z" },
limbBrace: { t: "rect", x: 20, y: 52, w: 16, h: 48, rx: 8 },
footBrace: { t: "path", d: "M46 136 L78 132 L84 188 L58 190 Z" }
}
},
/* Five bodies, one person. No vital, no head, every body an equal share. */
cadence: {
viewBox: "0 0 120 196",
parts: {
bodyA: { t: "ellipse", cx: 34, cy: 52, rx: 20, ry: 24 },
bodyB: { t: "ellipse", cx: 86, cy: 44, rx: 20, ry: 24 },
bodyC: { t: "ellipse", cx: 60, cy: 100, rx: 20, ry: 24 },
bodyD: { t: "ellipse", cx: 28, cy: 148, rx: 20, ry: 24 },
bodyE: { t: "ellipse", cx: 88, cy: 152, rx: 20, ry: 24 }
}
}
};