From ddc4f99ff49288de144903e0ed74538332bc94e0 Mon Sep 17 00:00:00 2001 From: slaguru666 <111923774+slaguru666@users.noreply.github.com> Date: Sat, 12 Sep 2026 15:46:11 +0100 Subject: [PATCH] Hit locations reach ordinary combat, and creatures get a body THE DEFECT. locationFor had ONE caller in the engine, hitLocationRoll, and hitLocationRoll was called from fall, burn, detonate and burstAttack. Falling, fire, explosions and automatic fire. Nothing else. rollWeaponDamage rolled dice, printed a number and returned it, so the chat Damage button, the character sheet's damage button and the item sheet's all stopped there. A grenade knew which arm it took off. A sword did not. Every individual piece was already correct - the tables, per-location armour and coverage, disable and destroy thresholds, Major Wound, dying, death, the figure on the sheet. They simply were not joined up, which is why this reads as a wiring change rather than a new subsystem. - rollWeaponDamage now carries through to hitLocationRoll: it rolls the location, applies that location's armour, applies the wound, and handles the rest through the machinery that already owned it. Nothing here re-implements any of that - locationModeFor picks the melee or ranged column from the weapon's class. The two columns have always differed and nothing was choosing between them - with no target it does exactly what it did before and SAYS SO on a card, rather than failing quietly - burstAttack opts out with locate:false - it rolls its own location per round, and letting both fire would have applied every round of a burst twice BODY PLANS. Two new tables: quadruped (four legs, fore/hindquarters, neck, a head that is hard to reach) and winged (that, plus wings). Forequarters are the vital. DISABLE EITHER WING AND IT CANNOT FLY, which is the fight a party can win. Barghest, Kelpie and Church grim were all species "baseline" - a black dog, a horse and a grave-hound, each hit-located with two arms and a chest. Fixed. Added "The supporter", a winged heraldic beast, so the new table is exercised by real content instead of existing unused. Body plan is a NEW FIELD, not a species. A bear is not a playable people with characteristic dice and talents, so creature specs take bodyPlan and the playable species list is untouched. THE NPC SHEET had no hit locations at all - 102 lines against the character sheet's 627 - while every NPC in the game had them derived and wired to consequences. It now carries the body-plan selector, the figure, the per-location table, the condition and a GROUNDED flag. The sheet class already extended the character sheet, so it already HAD the context; the template never used it. FOUND IN PASSING, by the new guard: kind "body" - the abdomen, the trunk, the hindquarters - did nothing whatever. Its printed effect has always promised "-30% to all Physical actions and bleeding 1 hit point per round until First Aid" and the code delivered none of it, on every body plan including the humanoid default. It now bleeds 1, or 2 destroyed, exactly as bodyX says. NEW GUARD: check-anatomy. Every body plan must cover 1-20 exactly once in BOTH modes, every location must be drawn and every drawn shape must be a location, and every kind must be one locationEffectsFor acts on, has effect text for, and has a destruction outcome. Verified it fails on a d20 gap, a shapeless limb and an unhandled kind before wiring it in. Ninth guard. VERIFIED IN A RUNNING FOUNDRY, not just unit-tested: melee bar reads the Melee column and the rifle reads Ranged on the same creature; forequarters graded as a vital hit; a maxed wing set flightLost and the sheet said GROUNDED; the real chat Damage button rolled 1D20 15, found the right foreleg, applied 12 through armour, disabled the leg and took the dog from 16 to 4; and with nothing targeted nothing was touched. Co-Authored-By: Claude Opus 5 --- README.md | 14 ++--- anatomy.mjs | 72 +++++++++++++++++++++++++ docs/BESTIARY.md | 25 ++++++++- lang/en.json | 22 +++++++- package.json | 2 +- packs/npcs/000003.log | Bin 1634535 -> 1670054 bytes ringbrp.mjs | 72 +++++++++++++++++++++++-- rules.mjs | 41 +++++++++++++-- styles/ringbrp.css | 36 +++++++++++++ system.json | 2 +- templates/npc-sheet.hbs | 39 ++++++++++++++ tools/bestiary.mjs | 6 ++- tools/build-packs.mjs | 8 ++- tools/check-anatomy.mjs | 108 ++++++++++++++++++++++++++++++++++++++ tools/check-behaviour.mjs | 76 +++++++++++++++++++++++++++ tools/check-rules.mjs | 15 ++++++ tools/content.mjs | 14 +++-- tools/creature-schema.mjs | 14 ++++- 18 files changed, 538 insertions(+), 28 deletions(-) create mode 100644 tools/check-anatomy.mjs 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 3b0954c27109242de9548fc814476236a81cd25e..d3eedb82a6ca413363cac25ab6b2b1887384fb78 100644 GIT binary patch delta 7842 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 + 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 @@
{{localize "RINGBRP.Sheet.MajorWound"}}{{sys.majorWound.value}}
{{localize "RINGBRP.Sheet.Armour"}}
+ {{!-- 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. --}} +
{{localize "RINGBRP.Sheet.BodyPlan"}} +
@@ -29,6 +38,8 @@
{{localize "RINGBRP.Sheet.DamageMod"}}{{sys.damageModifier}}
{{localize "RINGBRP.Sheet.MOV"}}{{sys.mov}}
+
{{localize "RINGBRP.Sheet.Condition"}}{{localize (rbConcat "RINGBRP.Condition." sys.condition)}}
+ {{#if sys.wounded.flightLost}}
{{localize "RINGBRP.Sheet.Grounded"}}
{{/if}}