simulate: locate the damage
The harness measured a game in which every blow went to a general pool. That was
tolerable while every creature was a baseline human and merely wrong once the bestiary
had a Cadence in it: a creature whose entire mechanical identity is being whittled down
body by body was being measured as a person with an odd portrait, and the number it
printed was about nobody.
locationFor and resolveLocationHit move from ringbrp.mjs to rules.mjs, and the
category mapping inside woundPenaltyFor becomes woundPenaltyFrom beside them, for the
reason applyDifficulty and resolveBands moved before them: node cannot load the engine,
and the harness may not own a second copy of a rule. All three arrive with the
spot-checks they never had - 103 rules now, 346 formulas verified.
What the loop does now: 1d20 against the DEFENDER's own species table, melee finding
limbs and shooting finding centre of mass; damage to that location and to the general
pool, so the two agree; disabled at the location maximum and destroyed at twice it,
cumulatively. Then the consequences actually apply - a destroyed head is unconscious
immediately (conditionFor has always accepted that flag and was never passed it, so a
headshot used to leave the target swinging), a ruined arm costs -30 to every attack
including shooting, a ruined leg costs -30 to dodge, and each Cadence body lost costs
the whole creature -10 to everything.
Verified as mechanics rather than as numbers that moved: no species can return a
location it does not have across all 40 rolls; vesh and cadence have no head at any
roll and cadence has no vital either; a vesh ridge is hit 3/20 where a human head is
1/20, which is the "long target rather than a small one" the tables were designed for.
A built Cadence has five bodies at 4 points each and no vital; a built Vesh has six
locations with the ridge the largest.
Measured consequences, seed 11, 400 runs, four duty-roster agents:
keepers x6 31.0% -> 36.5% wiped
the_choir x11 0.3% -> 0.0% wiped, and now degrading as bodies go rather than
being a person who cannot be shot in the head
long_walker 42.0% wiped, which nothing had ever measured
Still no bleeding, panic, Coherence, cover, range bands or fire modes. Still a floor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f88c31649c
commit
94479641b3
@@ -180,7 +180,7 @@ The build runs all seven guards before it writes anything:
|
||||
|
||||
<!-- guards:start -->
|
||||
```
|
||||
check-rules: OK — 99 rules, 31 files scanned, 3 aliases + 46 constant sets checked, 321 formulas verified
|
||||
check-rules: OK — 103 rules, 31 files scanned, 3 aliases + 46 constant sets checked, 346 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-behaviour: OK — 67 behavioural tests
|
||||
|
||||
+8
-37
@@ -24,6 +24,7 @@ import {
|
||||
locationMaxHp, reflexRating, awarenessRating, encumbranceFrom, reactionBaseFrom,
|
||||
reactionLoadFrom, reactionPrintedFrom, woundEffectsFor,
|
||||
BLEED, bleedStoppedBy, bleedRateFor, locationEffectsFor,
|
||||
locationFor, resolveLocationHit, locationsFor, woundPenaltyFrom,
|
||||
encumbrancePenaltyFor, ENCUMBERED_CATEGORIES, freeCarryFor,
|
||||
defencePenaltyFor, defenceTypeAllowed, DEFENCE_STEP,
|
||||
defenceOutcomeFor, levelRank, LEVEL_LADDER, skillBaseFrom,
|
||||
@@ -57,7 +58,8 @@ import {
|
||||
DEVELOPMENT, developmentMarksOn, developmentImproves, developmentGainFor
|
||||
} from "./rules.mjs";
|
||||
import { LOCATION_TABLES, BODY_ART } from "./anatomy.mjs";
|
||||
export { hitPointsFor, majorWoundFor, damageModifierFor, locationMaxHp, reflexRating, awarenessRating };
|
||||
export { hitPointsFor, majorWoundFor, damageModifierFor, locationMaxHp, reflexRating, awarenessRating,
|
||||
locationFor, resolveLocationHit, locationsFor };
|
||||
|
||||
function rbRenderTemplate(path, data) {
|
||||
const fn = foundry?.applications?.handlebars?.renderTemplate ?? globalThis.renderTemplate;
|
||||
@@ -426,34 +428,8 @@ export function resolveHit({ level, rolled, armour, majorWoundThreshold }) {
|
||||
};
|
||||
}
|
||||
|
||||
/** Spec §3.6. Which location a 1D20 finds, for this species and attack type. */
|
||||
export function locationFor(roll, speciesId = "baseline", mode = "ranged") {
|
||||
const table = RINGBRP.locationTables[speciesId] ?? RINGBRP.locationTables.baseline;
|
||||
const key = mode === "melee" ? "melee" : "ranged";
|
||||
return table.locations.find(l => roll >= l[key][0] && roll <= l[key][1]) ?? table.locations[0];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Spec §3.6. Resolve a hit against a location.
|
||||
* A blow that meets the location's maximum disables it; twice that destroys it.
|
||||
* General hit points still take the damage, so the two systems agree.
|
||||
*/
|
||||
export function resolveLocationHit({ damage, locationMax, locationTaken = 0, majorWoundThreshold }) {
|
||||
const d = Number(damage) || 0;
|
||||
const after = locationTaken + d;
|
||||
return {
|
||||
damage: d,
|
||||
locationTaken: after,
|
||||
disabled: after >= locationMax,
|
||||
// Cumulative, like `disabled` and like the actor's own derived state. This
|
||||
// used to test THIS hit's damage alone, so a limb could never be destroyed by
|
||||
// repeated wounds and the resolver disagreed with the sheet about the same
|
||||
// limb: derived data said destroyed, the resolver said not.
|
||||
destroyed: after >= locationMax * 2,
|
||||
majorWound: d >= (Number(majorWoundThreshold) || Infinity)
|
||||
};
|
||||
}
|
||||
/* locationFor and resolveLocationHit moved to rules.mjs so the lethality harness can
|
||||
import them — node cannot load this file. Re-exported below with the rest. */
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Documents */
|
||||
@@ -1379,15 +1355,10 @@ export const coherenceBand = coherenceBandFrom;
|
||||
* numbers add on top of it. Ranged and melee read the manipulation figure because
|
||||
* both are done with the hands, which is how the location wounds already worked.
|
||||
*/
|
||||
/* The mapping itself lives in rules.mjs as woundPenaltyFrom, so the lethality harness
|
||||
applies the same arithmetic to a plain object. This unwraps the actor, nothing more. */
|
||||
export function woundPenaltyFor(actor, categoryId) {
|
||||
const w = actor?.system?.wounded;
|
||||
if (!w) return 0;
|
||||
let pen = Number(w.all) || 0;
|
||||
if (["manip", "melee", "ranged"].includes(categoryId)) pen += Number(w.manipulation) || 0;
|
||||
if (categoryId === "phys") pen += Number(w.physical) || 0;
|
||||
if (categoryId === "percep") pen += Number(w.perception) || 0;
|
||||
if (categoryId === "comm") pen += Number(w.communication) || 0;
|
||||
return pen;
|
||||
return woundPenaltyFrom(actor?.system?.wounded, categoryId);
|
||||
}
|
||||
|
||||
export function coherencePenaltyFor(value, categoryId) {
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
* this file may import from the runtime or touch Foundry globals.
|
||||
*/
|
||||
|
||||
// The species anatomies. anatomy.mjs imports nothing and touches nothing, so this stays
|
||||
// inside the rule above: it is data the hit-location rules are defined over, and both
|
||||
// the engine and the node-side tools read that one copy.
|
||||
import { LOCATION_TABLES } from "./anatomy.mjs";
|
||||
|
||||
export const ceil = Math.ceil;
|
||||
|
||||
/** Hit points: half of CON + SIZ, standard BRP. */
|
||||
@@ -501,6 +506,71 @@ export function locationEffectsFor(locations = []) {
|
||||
* rucksack does not make you worse at Knowledge, and a rule that says otherwise is
|
||||
* a rule nobody will use.
|
||||
*/
|
||||
/* ---------------------------------------------------------------- hit locations */
|
||||
|
||||
/**
|
||||
* Spec §3.6. Which location a 1D20 finds, for this species and attack type.
|
||||
*
|
||||
* Moved out of ringbrp.mjs so node can reach it, for exactly the reason applyDifficulty,
|
||||
* resolveBands and gradeRoll were moved: the lethality harness could not model hit
|
||||
* locations while the only copy lived in the engine, and an unlocated Cadence is not a
|
||||
* Cadence — being whittled down body by body is the entire species. The tables come from
|
||||
* anatomy.mjs, which is the same one the engine reads.
|
||||
*/
|
||||
export function locationFor(roll, speciesId = "baseline", mode = "ranged") {
|
||||
const table = LOCATION_TABLES[speciesId] ?? LOCATION_TABLES.baseline;
|
||||
const key = mode === "melee" ? "melee" : "ranged";
|
||||
return table.locations.find(l => roll >= l[key][0] && roll <= l[key][1]) ?? table.locations[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Spec §3.6. Resolve a hit against a location.
|
||||
* A blow that meets the location's maximum disables it; twice that destroys it.
|
||||
* General hit points still take the damage, so the two systems agree.
|
||||
*/
|
||||
export function resolveLocationHit({ damage, locationMax, locationTaken = 0, majorWoundThreshold }) {
|
||||
const d = Number(damage) || 0;
|
||||
const after = locationTaken + d;
|
||||
return {
|
||||
damage: d,
|
||||
locationTaken: after,
|
||||
disabled: after >= locationMax,
|
||||
// Cumulative, like `disabled` and like the actor's own derived state. This used to
|
||||
// test THIS hit's damage alone, so a limb could never be destroyed by repeated
|
||||
// wounds and the resolver disagreed with the sheet about the same limb.
|
||||
destroyed: after >= locationMax * 2,
|
||||
majorWound: d >= (Number(majorWoundThreshold) || Infinity)
|
||||
};
|
||||
}
|
||||
|
||||
/** Every location a species has — what a combatant has to keep a tally for. */
|
||||
export function locationsFor(speciesId = "baseline") {
|
||||
return (LOCATION_TABLES[speciesId] ?? LOCATION_TABLES.baseline).locations;
|
||||
}
|
||||
|
||||
/**
|
||||
* How much a battered body costs a roll of one category.
|
||||
*
|
||||
* The mapping — `all` against everything, manipulation against anything you need hands
|
||||
* for, physical against phys, and so on — lived only inside ringbrp.mjs's
|
||||
* woundPenaltyFor, which reads a Foundry actor. The harness needs the same arithmetic
|
||||
* over a plain object, and writing the mapping a second time is the one thing this
|
||||
* repository does not permit. woundPenaltyFor now unwraps the actor and calls this.
|
||||
*
|
||||
* Note that melee and ranged both take the MANIPULATION penalty: a lost arm is what
|
||||
* stops you fighting, whether the weapon is a knife or a rifle. Dodge is `phys`, so
|
||||
* losing a leg is what stops you getting out of the way.
|
||||
*/
|
||||
export function woundPenaltyFrom(wounded, categoryId) {
|
||||
if (!wounded) return 0;
|
||||
let pen = Number(wounded.all) || 0;
|
||||
if (["manip", "melee", "ranged"].includes(categoryId)) pen += Number(wounded.manipulation) || 0;
|
||||
if (categoryId === "phys") pen += Number(wounded.physical) || 0;
|
||||
if (categoryId === "percep") pen += Number(wounded.perception) || 0;
|
||||
if (categoryId === "comm") pen += Number(wounded.communication) || 0;
|
||||
return pen;
|
||||
}
|
||||
|
||||
export const ENCUMBERED_CATEGORIES = ["phys", "melee", "ranged", "manip"];
|
||||
|
||||
export function encumbrancePenaltyFor(skillPenalty, categoryId) {
|
||||
|
||||
@@ -479,6 +479,40 @@ const checks = [
|
||||
["defenceOutcomeFor(special, special)", RULES.defenceOutcomeFor("special","special").turnedAside, true],
|
||||
["defenceOutcomeFor(success, success)", RULES.defenceOutcomeFor("success","success").turnedAside, true],
|
||||
["defenceOutcomeFor(success, fumble)", RULES.defenceOutcomeFor("success","fumble").landsAt, "success"],
|
||||
// Hit locations. Moved out of ringbrp.mjs so the harness can locate damage; these are
|
||||
// the spot-checks they never had while the engine held the only copy. A species that
|
||||
// has no head must not be able to return one, which is the whole point of the tables.
|
||||
["locationFor(20, baseline, melee) is the head", RULES.locationFor(20, "baseline", "melee").id, "head"],
|
||||
["locationFor(20, baseline, ranged) is the head", RULES.locationFor(20, "baseline", "ranged").id, "head"],
|
||||
["locationFor(1, baseline, melee) is a leg", RULES.locationFor(1, "baseline", "melee").kind, "leg"],
|
||||
// melee finds limbs, ranged finds centre of mass — the same roll, two answers
|
||||
["locationFor(11, baseline, melee) is abdomen", RULES.locationFor(11, "baseline", "melee").id, "abdomen"],
|
||||
["locationFor(11, baseline, ranged) is chest", RULES.locationFor(11, "baseline", "ranged").id, "chest"],
|
||||
// a vesh has no head at any roll; its vital is the ridge
|
||||
["locationFor(20, vesh, melee) is the ridge", RULES.locationFor(20, "vesh", "melee").id, "ridge"],
|
||||
["locationFor(20, vesh, ranged) is the ridge", RULES.locationFor(20, "vesh", "ranged").id, "ridge"],
|
||||
// a cadence is bodies all the way down — no head, no vital, whatever you roll
|
||||
["locationFor(20, cadence, melee) is a unit", RULES.locationFor(20, "cadence", "melee").kind, "unit"],
|
||||
["locationFor(1, cadence, ranged) is a unit", RULES.locationFor(1, "cadence", "ranged").kind, "unit"],
|
||||
["locationsFor(baseline) has 7", RULES.locationsFor("baseline").length, 7],
|
||||
["locationsFor(vesh) has 6", RULES.locationsFor("vesh").length, 6],
|
||||
["locationsFor(cadence) has 5", RULES.locationsFor("cadence").length, 5],
|
||||
["locationsFor(unknown) falls back to baseline", RULES.locationsFor("nonesuch").length, 7],
|
||||
// meeting the maximum disables; twice it destroys; and both are CUMULATIVE
|
||||
["resolveLocationHit(4 of 5) holds", RULES.resolveLocationHit({damage:4,locationMax:5}).disabled, false],
|
||||
["resolveLocationHit(5 of 5) disables", RULES.resolveLocationHit({damage:5,locationMax:5}).disabled, true],
|
||||
["resolveLocationHit(10 of 5) destroys", RULES.resolveLocationHit({damage:10,locationMax:5}).destroyed, true],
|
||||
["resolveLocationHit(3 onto 2 of 5) disables", RULES.resolveLocationHit({damage:3,locationMax:5,locationTaken:2}).disabled, true],
|
||||
["resolveLocationHit(3 onto 7 of 5) destroys", RULES.resolveLocationHit({damage:3,locationMax:5,locationTaken:7}).destroyed, true],
|
||||
// the wound-penalty mapping: all hits everything, manipulation stops you fighting,
|
||||
// physical stops you dodging
|
||||
["woundPenaltyFrom(all -10, melee)", RULES.woundPenaltyFrom({all:-10}, "melee"), -10],
|
||||
["woundPenaltyFrom(manip -30, melee)", RULES.woundPenaltyFrom({manipulation:-30}, "melee"), -30],
|
||||
["woundPenaltyFrom(manip -30, ranged)", RULES.woundPenaltyFrom({manipulation:-30}, "ranged"), -30],
|
||||
["woundPenaltyFrom(manip -30, phys)", RULES.woundPenaltyFrom({manipulation:-30}, "phys"), 0],
|
||||
["woundPenaltyFrom(phys -30, phys)", RULES.woundPenaltyFrom({physical:-30}, "phys"), -30],
|
||||
["woundPenaltyFrom(percep -30, percep)", RULES.woundPenaltyFrom({perception:-30}, "percep"), -30],
|
||||
["woundPenaltyFrom(nothing)", RULES.woundPenaltyFrom(null, "melee"), 0],
|
||||
// the Quiet Arts cost Coherence, and Unmake costs more than the rest
|
||||
["powerCostFor('thread')", RULES.powerCostFor("thread"), 1],
|
||||
["powerCostFor('thread', pushed)", RULES.powerCostFor("thread",{pushed:true}), 2],
|
||||
|
||||
+78
-12
@@ -26,16 +26,30 @@
|
||||
* WHAT IT MODELS, AND WHAT IT DOES NOT. It runs Reaction order re-rolled every round,
|
||||
* attack banding with the 1% floor and the 96-99 rule, one defence per incoming blow at
|
||||
* the stacking defence penalty, graded defences, damage by band with armour ignored on a
|
||||
* critical and halved on a special, major wounds, and the dying clock. It does NOT model
|
||||
* hit locations, bleeding, panic, Coherence, cover, range bands, fire modes or burst.
|
||||
* Those all exist in rules.mjs and all matter; they are not here yet, and every one of
|
||||
* them makes a fight WORSE for whoever is losing. Read the output as a floor on how bad
|
||||
* a creature is, never as a ceiling.
|
||||
* critical and halved on a special, major wounds, and the dying clock.
|
||||
*
|
||||
* It also, now, models HIT LOCATIONS — 1d20 against the defender's own species table,
|
||||
* melee finding limbs and shooting finding centre of mass, each location carrying its
|
||||
* own share of the pool, disabled at its maximum and destroyed at twice it. The
|
||||
* consequences apply: a destroyed head is unconscious immediately, a ruined arm costs
|
||||
* you every attack, a ruined leg costs you every dodge, and a Cadence losing a body
|
||||
* costs the whole creature 10% of everything it does.
|
||||
*
|
||||
* That last one is why this was worth doing. A Cadence has no vital and no head, so it
|
||||
* cannot be dropped by a lucky shot and instead degrades toward useless; measured
|
||||
* without locations it was simply a person with an odd portrait, and the number the
|
||||
* harness printed was about a creature nobody was playing.
|
||||
*
|
||||
* It still does NOT model bleeding, panic, Coherence, cover, range bands, fire modes or
|
||||
* burst. Those all exist in rules.mjs, they all matter, and every one of them makes a
|
||||
* fight WORSE for whoever is losing. Read the output as a floor, never as a ceiling.
|
||||
*/
|
||||
import {
|
||||
hitPointsFor, majorWoundFor, damageModifierFor, reactionBaseFrom,
|
||||
resolveBands, gradeRoll, defenceOutcomeFor, defencePenaltyFor,
|
||||
conditionFor, dyingLimitFor, REACTION
|
||||
conditionFor, dyingLimitFor, REACTION,
|
||||
locationFor, resolveLocationHit, locationsFor, locationMaxHp,
|
||||
locationEffectsFor, woundPenaltyFrom
|
||||
} from "../rules.mjs";
|
||||
import { WEAPONS, ARMOURS, NPCS, PREGENS } from "./content.mjs";
|
||||
import { ROSTER } from "./roster.mjs";
|
||||
@@ -145,8 +159,19 @@ export function buildCombatant(rawSpec, { side = "?" } = {}) {
|
||||
|
||||
const worn = (spec.armour ?? []).reduce((t, k) => t + (ARMOUR.get(k)?.points ?? 0), 0);
|
||||
|
||||
/* Every location this species has, each with its own share of the hit points. The
|
||||
share comes from the creature's OWN table, so a cadence gets five bodies at 0.28
|
||||
apiece and no vital at all, and a vesh gets a ridge instead of a head. */
|
||||
const species = spec.species ?? "baseline";
|
||||
const locations = locationsFor(species).map(l => ({
|
||||
id: l.id, kind: l.kind,
|
||||
max: locationMaxHp(hp, l.frac),
|
||||
taken: 0, disabled: false, destroyed: false
|
||||
}));
|
||||
|
||||
return {
|
||||
side, key: spec.key, name: spec.name,
|
||||
species,
|
||||
hpMax: hp, hp,
|
||||
majorWound: majorWoundFor(hp),
|
||||
majorWounds: 0,
|
||||
@@ -155,14 +180,20 @@ export function buildCombatant(rawSpec, { side = "?" } = {}) {
|
||||
armour: worn + (Number(spec.naturalArmour) || 0),
|
||||
dodge: skills.get("dodge:") ?? 0,
|
||||
arms,
|
||||
locations,
|
||||
wounded: locationEffectsFor(locations),
|
||||
defencesThisRound: 0,
|
||||
dyingRounds: 0,
|
||||
startedUp: true
|
||||
};
|
||||
}
|
||||
|
||||
/* A destroyed head is "unconscious immediately", which conditionFor has always known how
|
||||
to read and this harness never told it — so a headshot used to leave the target
|
||||
standing and swinging. */
|
||||
const conditionOf = c => conditionFor({
|
||||
hp: c.hp, dyingRounds: c.dyingRounds, majorWound: c.majorWounds
|
||||
hp: c.hp, dyingRounds: c.dyingRounds, majorWound: c.majorWounds,
|
||||
unconscious: !!c.wounded?.unconscious
|
||||
});
|
||||
const isFighting = c => conditionOf(c) === "up";
|
||||
|
||||
@@ -172,15 +203,24 @@ function attack(rng, attacker, defender) {
|
||||
const arm = attacker.arms[0];
|
||||
if (!arm) return;
|
||||
|
||||
const level = gradeRoll(d100(rng), resolveBands(arm.rating));
|
||||
// Where a blow lands depends on how it was thrown: melee finds limbs, because they are
|
||||
// between you and the target, and shooting finds centre of mass.
|
||||
const mode = arm.w.cls === "melee" ? "melee" : "ranged";
|
||||
|
||||
// A wounded attacker is a worse attacker. Melee and ranged both take the MANIPULATION
|
||||
// penalty — losing an arm is what stops you fighting, whatever is in your hand — and
|
||||
// the mapping comes from rules.mjs rather than being restated here.
|
||||
const atkPen = woundPenaltyFrom(attacker.wounded, mode);
|
||||
const level = gradeRoll(d100(rng), resolveBands(arm.rating, { situational: atkPen }));
|
||||
if (level === "fumble" || level === "failure") return;
|
||||
|
||||
// One defence per incoming blow, at the stacking penalty for each already spent this
|
||||
// round. A defender with nothing left still gets the roll — at a rating the penalty
|
||||
// has usually driven to the 1% floor, which resolveBands enforces rather than us.
|
||||
// round, PLUS whatever a ruined pair of legs costs — dodge is a `phys` skill, so a
|
||||
// disabled leg is exactly what stops you getting out of the way.
|
||||
let landing = level;
|
||||
if (defender.dodge > 0) {
|
||||
const penalty = defencePenaltyFor(defender.defencesThisRound);
|
||||
const penalty = defencePenaltyFor(defender.defencesThisRound)
|
||||
+ woundPenaltyFrom(defender.wounded, "phys");
|
||||
const dLevel = gradeRoll(d100(rng), resolveBands(defender.dodge, { situational: penalty }));
|
||||
defender.defencesThisRound++;
|
||||
const outcome = defenceOutcomeFor(level, dLevel);
|
||||
@@ -201,8 +241,34 @@ function attack(rng, attacker, defender) {
|
||||
|
||||
const taken = Math.max(0, dmg - armour);
|
||||
if (taken <= 0) return;
|
||||
if (taken >= defender.majorWound) defender.majorWounds++;
|
||||
|
||||
/* Locate it. 1d20 against the defender's own species table, then the damage goes to
|
||||
that location AND to general hit points — the two systems agree rather than
|
||||
competing, which is what resolveLocationHit is for.
|
||||
|
||||
This is where a Cadence stops being a person with a funny picture. Its five bodies
|
||||
each carry 0.28 of the pool, it has no vital and no head, so it cannot be dropped by
|
||||
one good shot; instead every body that goes costs the whole creature -10% to
|
||||
everything, and it degrades toward useless rather than dying. A Vesh, conversely,
|
||||
has a ridge for a vital that is a LONG target rather than a small one. None of that
|
||||
could be measured before, because damage was never located. */
|
||||
const loc = locationFor(d(rng, 20), defender.species, mode);
|
||||
const slot = defender.locations.find(l => l.id === loc.id) ?? defender.locations[0];
|
||||
const res = resolveLocationHit({
|
||||
damage: taken,
|
||||
locationMax: slot.max,
|
||||
locationTaken: slot.taken,
|
||||
majorWoundThreshold: defender.majorWound
|
||||
});
|
||||
slot.taken = res.locationTaken;
|
||||
slot.disabled = res.disabled;
|
||||
slot.destroyed = res.destroyed;
|
||||
if (res.majorWound) defender.majorWounds++;
|
||||
|
||||
defender.hp -= taken;
|
||||
// Recomputed from the whole body, not accumulated, because the effects fold together
|
||||
// rather than stacking per hit — two disabled legs are one condition, not two.
|
||||
defender.wounded = locationEffectsFor(defender.locations);
|
||||
}
|
||||
|
||||
/** One fight to a conclusion. Returns which side stood, and how long it took. */
|
||||
|
||||
Reference in New Issue
Block a user