diff --git a/README.md b/README.md
index 4c60bf7..d35e252 100644
--- a/README.md
+++ b/README.md
@@ -66,7 +66,7 @@ changing underneath a running server.
| **Trades** | 10 ordinary jobs the department recruits from, plus induction |
| **Catalogue** | 236 items — 123 gear, 62 weapons, 32 armour, 19 vehicles |
| **Skills / talents** | 62 skills, 19 talents |
-| **Bestiary** | 46 statblocks across folklore, horror and the far side |
+| **Bestiary** | 47 statblocks across folklore, horror and the far side |
| **Eras** | modern, antique, future, anomalous — the agency is old and never abolishes anything |
| **Icons** | 292, all generated from `tools/icons.mjs`; no external art |
| **Starter scenario** | **BLACK PLATFORM**, a six-player action starter, importable as a single Adventure |
@@ -181,13 +181,13 @@ The build runs all eight guards before it writes anything:
```
-check-rules: OK — 103 rules, 33 files scanned, 3 aliases + 46 constant sets checked, 346 formulas verified
+check-rules: OK — 103 rules, 34 files scanned, 3 aliases + 46 constant sets checked, 358 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, 995 keys, no leaf/branch collisions
+check-lang: OK — en.json, 1015 keys, no leaf/branch collisions
check-templates: OK — 19 templates compile
-check-behaviour: OK — 67 behavioural tests
-check-scenarios: OK — 9 scenario files, 62 mechanic tags, every skill named resolves against 62 catalogue entries
-check-creatures: OK — 101 actor specs across 8 sources, every characteristic, skill, kit key, talent, species and style resolves, no duplicate keys
-check-lethality: OK — 46 creatures, none drifted more than 6 points of wipe rate against the frozen party
+check-behaviour: OK — 75 behavioural tests
+check-scenarios: OK — 10 scenario files, 78 mechanic tags, every skill named resolves against 62 catalogue entries
+check-creatures: OK — 102 actor specs across 8 sources, every characteristic, skill, kit key, talent, species and style resolves, no duplicate keys
+check-lethality: OK — 47 creatures, none drifted more than 6 points of wipe rate against the frozen party, 1 new
```
diff --git a/anatomy.mjs b/anatomy.mjs
index 13a3e53..c6376f4 100644
--- a/anatomy.mjs
+++ b/anatomy.mjs
@@ -29,6 +29,45 @@ export const LOCATION_TABLES = {
{ id: "head", label: "RINGBRP.Loc.Head", melee: [19, 20], ranged: [20, 20], frac: 0.4, kind: "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" },
+ { id: "hindLegL", label: "RINGBRP.Loc.HindLegL", melee: [4, 6], ranged: [3, 4], frac: 0.42, kind: "leg" },
+ { id: "hindQtr", label: "RINGBRP.Loc.HindQtr", melee: [7, 9], ranged: [5, 8], frac: 0.42, kind: "body" },
+ { id: "foreQtr", label: "RINGBRP.Loc.ForeQtr", melee: [10, 12], ranged: [9, 13], frac: 0.5, kind: "vital"},
+ { id: "foreLegR", label: "RINGBRP.Loc.ForeLegR", melee: [13, 15], ranged: [14, 15], frac: 0.42, kind: "leg" },
+ { id: "foreLegL", label: "RINGBRP.Loc.ForeLegL", melee: [16, 18], ranged: [16, 17], frac: 0.42, kind: "leg" },
+ { id: "neck", label: "RINGBRP.Loc.Neck", melee: [19, 19], ranged: [18, 19], frac: 0.4, kind: "neck" },
+ { id: "head", label: "RINGBRP.Loc.Head", melee: [20, 20], ranged: [20, 20], frac: 0.4, kind: "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",
+ locations: [
+ { id: "hindLegR", label: "RINGBRP.Loc.HindLegR", melee: [1, 3], ranged: [1, 2], frac: 0.42, kind: "leg" },
+ { id: "hindLegL", label: "RINGBRP.Loc.HindLegL", melee: [4, 6], ranged: [3, 4], frac: 0.42, kind: "leg" },
+ { id: "hindQtr", label: "RINGBRP.Loc.HindQtr", melee: [7, 9], ranged: [5, 6], frac: 0.42, kind: "body" },
+ { id: "foreQtr", label: "RINGBRP.Loc.ForeQtr", melee: [10, 12], ranged: [7, 9], frac: 0.5, kind: "vital"},
+ { id: "foreLegR", label: "RINGBRP.Loc.ForeLegR", melee: [13, 14], ranged: [10, 10], frac: 0.42, kind: "leg" },
+ { id: "foreLegL", label: "RINGBRP.Loc.ForeLegL", melee: [15, 16], ranged: [11, 11], frac: 0.42, kind: "leg" },
+ { id: "wingR", label: "RINGBRP.Loc.WingR", melee: [17, 17], ranged: [12, 14], frac: 0.35, kind: "wing" },
+ { id: "wingL", label: "RINGBRP.Loc.WingL", melee: [18, 18], ranged: [15, 17], frac: 0.35, kind: "wing" },
+ { id: "neck", label: "RINGBRP.Loc.Neck", melee: [19, 19], ranged: [18, 19], frac: 0.4, kind: "neck" },
+ { id: "head", label: "RINGBRP.Loc.Head", melee: [20, 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
@@ -82,6 +121,39 @@ export const BODY_ART = {
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: {
diff --git a/docs/BESTIARY.md b/docs/BESTIARY.md
index d57f39d..5762914 100644
--- a/docs/BESTIARY.md
+++ b/docs/BESTIARY.md
@@ -3,7 +3,7 @@
*Generated by `tools/bestiary.mjs`. Do not edit: every number is derived from the
statblock or read from the lethality baseline, so this cannot disagree with the game.*
-46 creatures. 10 of them are not human-shaped.
+47 creatures. 10 of them are not human-shaped.
## At a glance
@@ -57,6 +57,7 @@ Sorted by how much of a four-agent party each one puts on the floor, alone. **So
| C | **The committee** | It went into session in 1953 and has not risen | 12 | 2 | 15 | Punch / kick 30% | 0.00 |
| | **The minutes** | A faithful record of a meeting that has not been held | 4 | 0 | 7 | Punch / kick 10% | 0.00 |
| | **The switchboard** | It is still connecting calls and some of them are answered | 14 | 4 | 17 | Punch / kick 25% | 0.00 |
+| | **The supporter** | The winged beast off a borough coat of arms, come down off it | 24 | 6 | 22 | Grasping limb 75% | — |
**V** = Vesh, **C** = Cadence. Everything else is baseline-human anatomy, including
several things that are plainly not human-shaped — see the note at the foot.
@@ -99,6 +100,8 @@ Older than the department and largely uninterested in it. Most of these are arra
*The black dog on the road out of the village*
+> **Four-legged** — four legs, no arms. The forequarters are the vital and take the most hits at range; the neck is worth aiming at and the head is hard to reach (1 face in melee).
+
| STR | CON | SIZ | INT | POW | DEX | Armour | HP | Major wound | Locations |
|---|---|---|---|---|---|---|---|---|---|
| 18 | 16 | 15 | 5 | 16 | 17 | 3 | 16 | 8 | 7 |
@@ -141,6 +144,8 @@ Older than the department and largely uninterested in it. Most of these are arra
*What was buried at the north door to keep the ground*
+> **Four-legged** — four legs, no arms. The forequarters are the vital and take the most hits at range; the neck is worth aiming at and the head is hard to reach (1 face in melee).
+
| STR | CON | SIZ | INT | POW | DEX | Armour | HP | Major wound | Locations |
|---|---|---|---|---|---|---|---|---|---|
| 16 | 20 | 14 | 7 | 19 | 11 | 5 | 17 | 9 | 7 |
@@ -155,6 +160,8 @@ Older than the department and largely uninterested in it. Most of these are arra
*The horse standing at the water that is not a horse*
+> **Four-legged** — four legs, no arms. The forequarters are the vital and take the most hits at range; the neck is worth aiming at and the head is hard to reach (1 face in melee).
+
| STR | CON | SIZ | INT | POW | DEX | Armour | HP | Major wound | Locations |
|---|---|---|---|---|---|---|---|---|---|
| 21 | 15 | 18 | 9 | 18 | 13 | 2 | 17 | 9 | 7 |
@@ -251,6 +258,20 @@ Older than the department and largely uninterested in it. Most of these are arra
*Measured alone against the frozen party: 0.00 of 4 down, 0% wiped, 2 rounds.*
+### The supporter
+
+*The winged beast off a borough coat of arms, come down off it*
+
+> **Four-legged, winged** — as the four-legged, plus wings. **Disable either wing and it cannot fly**, which is the fight the players can win. Wings are 6 faces of 20 at range and 2 in melee.
+
+| STR | CON | SIZ | INT | POW | DEX | Armour | HP | Major wound | Locations |
+|---|---|---|---|---|---|---|---|---|---|
+| 26 | 20 | 24 | 8 | 17 | 12 | 6 | 22 | 11 | 7 |
+
+**Attacks with** Grasping limb at 75%, 1d6+1 · **dodges** 35%
+
+**ARGENT AND GULES.** While it stands on ground the borough holds, it takes half damage from anything the borough owns — which includes the department's vehicles and any weapon signed out of Stores. WING IT.
+
---
## Horror
@@ -760,7 +781,7 @@ as a floor.
## A known gap
-21 creatures fight with nothing but their own bodies and still carry
+22 creatures fight with nothing but their own bodies and still carry
baseline-human anatomy. Some of those are people and that is fine. Several plainly are
not — a black dog, a case file that eats the cases filed next to it, a machine that has
decided it is the site — and all of them are hit-located as people, so you can shoot the
diff --git a/lang/en.json b/lang/en.json
index 8907e2a..273f700 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -464,6 +464,7 @@
"RINGBRP.Loc.Disabled": "disabled",
"RINGBRP.Loc.Destroyed": "destroyed",
"RINGBRP.Loc.Header": "Hit location",
+ "RINGBRP.Loc.NoTarget": "No target — damage rolled, but nothing was hit. Target a token and roll damage again to place the blow.",
"RINGBRP.Loc.RollHint": "Roll 1D20 for a hit location on this anatomy",
"RINGBRP.Tab.Anatomy": "Anatomy",
"RINGBRP.Sheet.LocationNote": "A blow meeting a location's maximum disables it; twice that destroys it. Damage also comes off general hit points, so the two readings always agree.",
@@ -895,6 +896,7 @@
"RINGBRP.Coverage.torsoArms": "torso and arms",
"RINGBRP.Coverage.torsoLimbs": "torso, arms and legs",
"RINGBRP.Coverage.head": "head",
+ "RINGBRP.Coverage.barding": "Barding (barrel, neck and legs)",
"RINGBRP.Sheet.Unarmoured": "bare",
"RINGBRP.Sheet.ByLocation": "depending where you are hit",
"RINGBRP.Item.Coverage": "Covers",
@@ -993,5 +995,23 @@
"RINGBRP.Dev.GMOnly": "Only the GM can open the development phase.",
"RINGBRP.Dev.Button": "Development",
"RINGBRP.Dev.NotOpen": "The GM has not opened the development phase.",
- "RINGBRP.Dev.Tooltip": "Roll to improve the skills that failed you this case. A skill improves if the roll is ANOTHER failure."
+ "RINGBRP.Dev.Tooltip": "Roll to improve the skills that failed you this case. A skill improves if the roll is ANOTHER failure.",
+ "RINGBRP.Species.Quadruped": "Four-legged",
+ "RINGBRP.Species.Winged": "Four-legged, winged",
+ "RINGBRP.Loc.HindLegR": "Right hind leg",
+ "RINGBRP.Loc.HindLegL": "Left hind leg",
+ "RINGBRP.Loc.ForeLegR": "Right foreleg",
+ "RINGBRP.Loc.ForeLegL": "Left foreleg",
+ "RINGBRP.Loc.HindQtr": "Hindquarters",
+ "RINGBRP.Loc.ForeQtr": "Forequarters",
+ "RINGBRP.Loc.Neck": "Neck",
+ "RINGBRP.Loc.WingR": "Right wing",
+ "RINGBRP.Loc.WingL": "Left wing",
+ "RINGBRP.LocEffect.neck": "Down, and staying down. Nothing holds its head up.",
+ "RINGBRP.LocEffect.neckX": "Severed. This is death for anything that carries a head.",
+ "RINGBRP.LocEffect.wing": "Grounded. It cannot fly on this, and it comes down if it was flying.",
+ "RINGBRP.LocEffect.wingX": "Torn away. It will never fly again.",
+ "RINGBRP.Sheet.BodyPlan": "Body plan",
+ "RINGBRP.Sheet.Grounded": "GROUNDED — a wing is gone",
+ "RINGBRP.Sheet.Condition": "State"
}
diff --git a/package.json b/package.json
index 231e278..c6f3940 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
"bestiary": "node tools/bestiary.mjs",
"mj": "node tools/mj-queue.mjs",
"simulate": "node tools/simulate.mjs",
- "check": "bun tools/check-rules.mjs && bun tools/check-kits.mjs && bun tools/check-lang.mjs && bun tools/check-templates.mjs && bun tools/check-behaviour.mjs && bun tools/check-scenarios.mjs && bun tools/check-creatures.mjs && bun tools/check-lethality.mjs",
+ "check": "bun tools/check-rules.mjs && bun tools/check-kits.mjs && bun tools/check-lang.mjs && bun tools/check-templates.mjs && bun tools/check-behaviour.mjs && bun tools/check-scenarios.mjs && bun tools/check-creatures.mjs && bun tools/check-anatomy.mjs && bun tools/check-lethality.mjs",
"test": "bun run check",
"readme": "bun tools/update-readme.mjs"
},
diff --git a/packs/npcs/000003.log b/packs/npcs/000003.log
index 3b0954c..d3eedb8 100644
Binary files a/packs/npcs/000003.log and b/packs/npcs/000003.log differ
diff --git a/ringbrp.mjs b/ringbrp.mjs
index 6f64e2b..c8ae68e 100644
--- a/ringbrp.mjs
+++ b/ringbrp.mjs
@@ -207,7 +207,9 @@ RINGBRP.locationEffects = {
unit: { disabled: "RINGBRP.LocEffect.unit", destroyed: "RINGBRP.LocEffect.unitX" },
body: { disabled: "RINGBRP.LocEffect.body", destroyed: "RINGBRP.LocEffect.bodyX" },
vital: { disabled: "RINGBRP.LocEffect.vital", destroyed: "RINGBRP.LocEffect.vitalX" },
- head: { disabled: "RINGBRP.LocEffect.head", destroyed: "RINGBRP.LocEffect.headX" }
+ head: { disabled: "RINGBRP.LocEffect.head", destroyed: "RINGBRP.LocEffect.headX" },
+ neck: { disabled: "RINGBRP.LocEffect.neck", destroyed: "RINGBRP.LocEffect.neckX" },
+ wing: { disabled: "RINGBRP.LocEffect.wing", destroyed: "RINGBRP.LocEffect.wingX" }
};
// Not a copy — rules.mjs owns the modes; this is the keyed view the item sheet wants,
@@ -597,13 +599,16 @@ class RingBRPActor extends Actor {
sys.wounded = {
legs: loc.legs, legsDestroyed: loc.legsDestroyed, arms: loc.arms,
vital: loc.vital, unitsLost: loc.unitsLost,
+ wings: loc.wings, flightLost: loc.flightLost,
movFactor: loc.movFactor * mw.movFactor,
manipulation: loc.manipulation + mw.manipulation,
physical: loc.physical + mw.physical,
perception: loc.perception + mw.perception,
communication: mw.communication,
all: loc.all + mw.all,
- bleed: mw.bleed,
+ // Bleeding from an opened body cavity and bleeding from a Major Wound are
+ // two holes in the same person, so they add.
+ bleed: loc.bleed + mw.bleed,
defenceOnly: mw.defenceOnly,
unconscious: loc.unconscious || mw.unconscious,
lostAction: mw.lostAction,
@@ -2496,7 +2501,10 @@ export async function burstAttack(actor, weapon, target, { size = "short",
for (let i = 0; i < hits; i++) {
// A critical burst puts its FIRST round through armour and the rest normally.
const level = (res.level === "critical" && i === 0) ? "critical" : "success";
- const dmg = await rollWeaponDamage({ actor, weapon, level });
+ // locate:false — this loop does its own location roll per round below, and
+ // rollWeaponDamage now locates by default. Letting both fire would apply every
+ // round of the burst twice.
+ const dmg = await rollWeaponDamage({ actor, weapon, level, locate: false });
const hl = await hitLocationRoll({ target, damage: dmg, mode: "ranged", level });
if (hl) lines.push(`${dmg} → ${game.i18n.localize(hl.location.label)}`);
// ONE Major Wound per burst, however many rounds connected. A burst is a single
@@ -5807,8 +5815,44 @@ async function promptAttackOptions({ weapon, actor }) {
* when the hit is resolved, so what changes here is the weapon dice and the note
* the card carries.
*/
+/**
+ * Which column of the location table a weapon reads.
+ *
+ * The tables carry separate melee and ranged spreads — a man with a spear is down
+ * among the legs, a man with a rifle is looking at a broadside — and nothing was
+ * choosing between them for an ordinary attack, because an ordinary attack never
+ * rolled a location at all.
+ */
+export function locationModeFor(weapon) {
+ return (weapon?.system?.weaponClass || "melee") === "melee" ? "melee" : "ranged";
+}
+
+/** The token this damage is for: the one named, else the one the user has targeted. */
+function damageTargetFor(explicit = null) {
+ if (explicit) return explicit;
+ const t = [...(game.user?.targets ?? [])][0];
+ return t?.actor ?? null;
+}
+
+/**
+ * Roll a weapon's damage — and, since there is a hit location system, find out WHERE.
+ *
+ * This used to roll dice, print a number and stop. Every location in the game was
+ * derived, armoured, drawn on the sheet and wired to real consequences, and the only
+ * things that ever reached it were falling, fire, explosions and automatic fire. A
+ * grenade knew which arm it took off; a sword did not. Hit locations were effectively
+ * a hazards-only subsystem while the players spent every fight swinging and shooting.
+ *
+ * So the damage roll now carries through to `hitLocationRoll`, which already owned all
+ * of this: the d20, location armour, disable and destroy thresholds, the Major Wound
+ * check, hit points, dying and death. Nothing here re-implements any of it.
+ *
+ * With no target, it does exactly what it did before and says so — the card reports
+ * the damage and nothing is touched. That is the fallback, not the intent: target a
+ * token and the blow lands somewhere.
+ */
export async function rollWeaponDamage({ actor = null, weapon, level = "success", band = null,
- bonus = "" } = {}) {
+ bonus = "", target = null, locate = true } = {}) {
if (!weapon) return null;
const d = weapon.system?.damage ?? {};
let formula = d.formula || "1d6";
@@ -5854,6 +5898,24 @@ export async function rollWeaponDamage({ actor = null, weapon, level = "success"
speaker: ChatMessage.getSpeaker({ actor: actor ?? undefined }),
content
});
+
+ // Where it landed. hitLocationRoll applies the location's own armour, so the damage
+ // goes through RAW — handing it an already-reduced number would charge armour twice.
+ if (locate) {
+ const victim = damageTargetFor(target);
+ if (victim) {
+ await hitLocationRoll({ target: victim, damage: roll.total,
+ mode: locationModeFor(weapon), level });
+ } else {
+ // Say so rather than failing quietly. A GM who meant to target somebody needs
+ // to know the blow went nowhere, and one who did not is told how to make it.
+ await ChatMessage.create({
+ speaker: ChatMessage.getSpeaker({ actor: actor ?? undefined }),
+ content: `
${
+ game.i18n.localize("RINGBRP.Loc.NoTarget")}
`
+ });
+ }
+ }
return roll.total;
}
@@ -6174,7 +6236,7 @@ Hooks.once("init", () => {
TIERS, THREATS, SPECIES,
npcTarget, npcShouldWithdraw, declareNpcTarget,
setReaction,
- locationFor, locationMaxHp, resolveLocationHit, hitLocationRoll
+ locationFor, locationMaxHp, resolveLocationHit, hitLocationRoll, locationModeFor
};
});
diff --git a/rules.mjs b/rules.mjs
index 4dcc309..a23d25c 100644
--- a/rules.mjs
+++ b/rules.mjs
@@ -460,8 +460,9 @@ export function coherenceBandFrom(value) {
*/
export function locationEffectsFor(locations = []) {
const eff = { all: 0, manipulation: 0, physical: 0, perception: 0,
- movFactor: 1, unconscious: false, prone: false,
- legs: 0, legsDestroyed: 0, arms: 0, vital: false, unitsLost: 0 };
+ movFactor: 1, unconscious: false, prone: false, bleed: 0,
+ legs: 0, legsDestroyed: 0, arms: 0, vital: false, unitsLost: 0,
+ wings: 0, flightLost: false };
for (const l of Array.isArray(locations) ? locations : []) {
if (!l?.disabled) continue;
switch (l.kind) {
@@ -482,16 +483,40 @@ export function locationEffectsFor(locations = []) {
eff.unconscious = true; // "unconscious immediately"
eff.vital = true;
break;
+ case "body":
+ // The printed effect has always read "-30% to all Physical actions and
+ // bleeding 1 hit point per round until First Aid", and nothing delivered a
+ // point of it. An opened abdomen was decoration on every body plan in the
+ // game, the humanoid default included. Destroyed doubles the bleed, which
+ // is what bodyX says.
+ eff.physical -= 30;
+ eff.bleed += l.destroyed ? 2 : 1;
+ break;
case "vital":
eff.vital = true; // knocked down; the Stamina roll is the GM's call
break;
+ case "neck":
+ // A broken neck puts the animal on the ground and keeps it there. This is
+ // the location that makes a spear worth aiming rather than swinging.
+ eff.unconscious = true;
+ eff.vital = true;
+ break;
+ case "wing":
+ // ONE wing is enough. A flyer with a hole in it comes down, and coming down
+ // is the whole reason a table would shoot at a wing instead of the body.
+ eff.wings++;
+ eff.flightLost = true;
+ eff.physical -= 10;
+ break;
case "unit":
eff.unitsLost++;
eff.all -= 10; // each body lost costs the WHOLE person
break;
}
}
- // One leg gone is a limp; both, or a destroyed one, is the floor.
+ // One leg gone is a limp; both, or a destroyed one, is the floor. This counts legs
+ // rather than assuming two of them, so a quadruped that has lost one of four is
+ // lamed and not felled — which is the difference between a horse and a man.
eff.movFactor = eff.legsDestroyed ? 0.1 : (eff.legs ? 0.5 : 1);
eff.prone = eff.legsDestroyed > 0 || eff.vital;
return eff;
@@ -1042,7 +1067,12 @@ export const COVERAGE = {
kinds: ["body", "vital", "ridge", "unit", "arm", "grasp"] },
torsoLimbs: { id: "torsoLimbs", label: "RINGBRP.Coverage.torsoLimbs",
kinds: ["body", "vital", "ridge", "unit", "arm", "grasp", "leg", "brace"] },
- head: { id: "head", label: "RINGBRP.Coverage.head", kinds: ["head"] }
+ head: { id: "head", label: "RINGBRP.Coverage.head", kinds: ["head"] },
+ // Armour that fits an animal rather than a man: barding covers the barrel, the
+ // neck and the legs, and reaches no wing. A cuirass does none of this, which is
+ // why it is a set of its own rather than another kind bolted onto torsoLimbs.
+ barding: { id: "barding", label: "RINGBRP.Coverage.barding",
+ kinds: ["body", "vital", "neck", "leg"] }
};
export function coverageFor(id) {
@@ -1325,8 +1355,9 @@ export function destructionOutcomeFor(kind, destroyed = false) {
if (!destroyed) return "none";
switch (kind) {
case "head": return "dead"; // whatever keeps its brain there no longer does
+ case "neck": return "dead"; // nothing carries a head on a severed neck
case "ridge": case "vital": return "dying";
- default: return "none"; // limbs are ruinous, not fatal
+ default: return "none"; // limbs AND WINGS are ruinous, not fatal
}
}
diff --git a/styles/ringbrp.css b/styles/ringbrp.css
index 3d61bb8..0a51b43 100644
--- a/styles/ringbrp.css
+++ b/styles/ringbrp.css
@@ -1977,3 +1977,39 @@
text-transform: uppercase; color: var(--rb-danger);
}
.rb-dev-open { color: var(--rb-yellow); font-size: 13px; margin: 4px 0; }
+
+/* --- NPC anatomy -------------------------------------------------------------
+ The NPC sheet carried no hit locations at all while every NPC in the game had
+ them derived, armoured and wired to consequences. The figure and the table sit
+ side by side on the combat tab: the drawing for a glance, the numbers to edit. */
+.rb-npc-anatomy {
+ display: grid;
+ grid-template-columns: minmax(0, 140px) minmax(0, 1fr);
+ gap: 12px;
+ align-items: start;
+ margin-bottom: 16px;
+}
+.rb-npc-figure { display: flex; justify-content: center; }
+.rb-npc-figure .rb-body-svg { width: 100%; height: auto; max-height: 190px; }
+.rb-npc-anatomy .rb-loctable { width: 100%; }
+.rb-npc-anatomy .rb-loc-taken { width: 3.2em; text-align: right; }
+
+/* The body-plan selector in the NPC header, beside natural armour. The select itself
+ reuses .rb-species-select from the character sheet — one control, one look. */
+.rb-species { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
+.rb-species .rb-species-select { width: 100%; max-width: 12em; margin-left: 0; }
+
+/* A flyer that has lost a wing has lost the fight it was going to have. */
+.rb-flag-bad {
+ color: var(--rb-danger);
+ font-weight: 600;
+ letter-spacing: 0.02em;
+}
+.rb-cond-dying, .rb-cond-dead { color: var(--rb-danger); font-weight: 700; }
+
+/* Damage rolled with nobody targeted. Quiet, but not silent. */
+.rb-noloc { opacity: 0.8; font-style: italic; }
+
+@media (max-width: 620px) {
+ .rb-npc-anatomy { grid-template-columns: minmax(0, 1fr); }
+}
diff --git a/system.json b/system.json
index 6a34b4f..2df0bc2 100644
--- a/system.json
+++ b/system.json
@@ -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.6.0",
+ "version": "1.7.0",
"compatibility": {
"minimum": 13,
"verified": "14.364"
diff --git a/templates/npc-sheet.hbs b/templates/npc-sheet.hbs
index efde91a..9eb8ac2 100644
--- a/templates/npc-sheet.hbs
+++ b/templates/npc-sheet.hbs
@@ -14,6 +14,15 @@
+ {{!-- 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. --}}
+