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 @@
{{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}}