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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
748e061b56
commit
ddc4f99ff4
+67
-5
@@ -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: `<div class="ringbrp-chat rb-chat-flavour rb-noloc">${
|
||||
game.i18n.localize("RINGBRP.Loc.NoTarget")}</div>`
|
||||
});
|
||||
}
|
||||
}
|
||||
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
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user