Files
RingBRP/anatomy.mjs
T
slaguru666andClaude Opus 5 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>
2026-08-31 20:32:30 +01:00

110 lines
6.0 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" },
{ id: "legL", label: "RINGBRP.Loc.LegL", melee: [5, 8], ranged: [4, 6], frac: 0.42, kind: "leg" },
{ id: "abdomen", label: "RINGBRP.Loc.Abdomen", melee: [9, 11], ranged: [7, 10], frac: 0.42, kind: "body" },
{ id: "chest", label: "RINGBRP.Loc.Chest", melee: [12, 12], ranged: [11, 15], frac: 0.5, kind: "vital"},
{ id: "armR", label: "RINGBRP.Loc.ArmR", melee: [13, 15], ranged: [16, 17], frac: 0.35, kind: "arm" },
{ id: "armL", label: "RINGBRP.Loc.ArmL", melee: [16, 18], ranged: [18, 19], frac: 0.35, kind: "arm" },
{ id: "head", label: "RINGBRP.Loc.Head", melee: [19, 20], ranged: [20, 20], frac: 0.4, kind: "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" },
{ id: "limbBrace", label: "RINGBRP.Loc.BraceLimb", melee: [4, 8], ranged: [4, 7], frac: 0.42, kind: "brace" },
{ id: "lowTrunk", label: "RINGBRP.Loc.LowerTrunk", melee: [9, 12], ranged: [8, 12], frac: 0.42, kind: "body" },
{ id: "upTrunk", label: "RINGBRP.Loc.UpperTrunk", melee: [13, 15], ranged: [13, 15], frac: 0.42, kind: "body" },
{ id: "limbGrasp", label: "RINGBRP.Loc.GraspLimb", melee: [16, 18], ranged: [16, 17], frac: 0.35, kind: "grasp" },
{ id: "ridge", label: "RINGBRP.Loc.Ridge", melee: [19, 20], ranged: [18, 20], frac: 0.5, kind: "ridge" }
]
},
/* 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" },
{ id: "bodyB", label: "RINGBRP.Loc.BodyB", melee: [5, 8], ranged: [5, 8], frac: 0.28, kind: "unit" },
{ id: "bodyC", label: "RINGBRP.Loc.BodyC", melee: [9, 12], ranged: [9, 12], frac: 0.28, kind: "unit" },
{ id: "bodyD", label: "RINGBRP.Loc.BodyD", melee: [13, 16], ranged: [13, 16], frac: 0.28, kind: "unit" },
{ id: "bodyE", label: "RINGBRP.Loc.BodyE", melee: [17, 20], ranged: [17, 20], frac: 0.28, kind: "unit" }
]
}
};
/**
* 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 }
}
},
/* 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 }
}
}
};