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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0633589061
commit
255b146753
+109
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 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 }
|
||||
}
|
||||
}
|
||||
};
|
||||
+6
-95
@@ -56,6 +56,7 @@ import {
|
||||
panicBandFrom, panicPenaltyFor, panicBreaksAt, panicAfter, panicReliefFor,
|
||||
DEVELOPMENT, developmentMarksOn, developmentImproves, developmentGainFor
|
||||
} from "./rules.mjs";
|
||||
import { LOCATION_TABLES, BODY_ART } from "./anatomy.mjs";
|
||||
export { hitPointsFor, majorWoundFor, damageModifierFor, locationMaxHp, reflexRating, awarenessRating };
|
||||
|
||||
function rbRenderTemplate(path, data) {
|
||||
@@ -187,101 +188,11 @@ RINGBRP.majorWoundTable = MAJOR_WOUND_TABLE;
|
||||
* Melee and ranged distributions differ: melee finds limbs because
|
||||
* they are between you and the target; ranged finds centre of mass.
|
||||
* ============================================================ */
|
||||
/* 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%. */
|
||||
RINGBRP.locationTables = {
|
||||
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.
|
||||
*/
|
||||
RINGBRP.bodyArt = {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
};
|
||||
/* Location tables and body geometry live in anatomy.mjs so the node-side tools can read
|
||||
them too — a portrait generated from the creature's own geometry cannot contradict its
|
||||
hit-location table. The engine reads that one definition rather than a copy of it. */
|
||||
RINGBRP.locationTables = LOCATION_TABLES;
|
||||
RINGBRP.bodyArt = BODY_ART;
|
||||
|
||||
/** What losing a location does, by anatomical KIND rather than by name,
|
||||
* so a new species inherits sensible consequences for free. */
|
||||
|
||||
@@ -97,10 +97,13 @@ for (const name of constants) {
|
||||
* because the sheet falls back to baseline, so three of four agents were quietly
|
||||
* drawn and hit-located as something the game does not define. */
|
||||
{
|
||||
const engineSrc = await readFile(new URL("ringbrp.mjs", root), "utf8");
|
||||
const tablesBlock = engineSrc.match(/^RINGBRP\.locationTables = \{([\s\S]*?)^\};/m);
|
||||
const profiles = tablesBlock
|
||||
? [...tablesBlock[1].matchAll(/^ {2}(\w+):\s*\{/gm)].map(x => x[1]) : [];
|
||||
// Imported rather than scraped out of the engine source. The tables moved to
|
||||
// anatomy.mjs so node-side tools could read them, and this guard — which matched them
|
||||
// with a regex against ringbrp.mjs — went blind the moment they did. A guard that
|
||||
// parses source text fails silently-shaped when the source is refactored; one that
|
||||
// imports the value cannot.
|
||||
const { LOCATION_TABLES } = await import("../anatomy.mjs");
|
||||
const profiles = Object.keys(LOCATION_TABLES);
|
||||
const tpl = JSON.parse(await readFile(new URL("template.json", root), "utf8"));
|
||||
const found = [];
|
||||
(function walk(o) {
|
||||
|
||||
Reference in New Issue
Block a user