@alpaca-software/40kdc-data 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cruncher/buffs.d.ts +57 -1
- package/dist/cruncher/buffs.d.ts.map +1 -1
- package/dist/cruncher/buffs.js +32 -3
- package/dist/cruncher/buffs.js.map +1 -1
- package/dist/cruncher/engine.d.ts.map +1 -1
- package/dist/cruncher/engine.js +50 -15
- package/dist/cruncher/engine.js.map +1 -1
- package/dist/cruncher/from-dsl.d.ts.map +1 -1
- package/dist/cruncher/from-dsl.js +121 -6
- package/dist/cruncher/from-dsl.js.map +1 -1
- package/dist/data/bundle.generated.js +1 -1
- package/dist/data/bundle.generated.js.map +1 -1
- package/dist/data/normalize.d.ts.map +1 -1
- package/dist/data/normalize.js +8 -1
- package/dist/data/normalize.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +69 -24
- package/dist/runner.js.map +1 -1
- package/dist/scrub-defensive-flag.d.ts +23 -0
- package/dist/scrub-defensive-flag.d.ts.map +1 -0
- package/dist/scrub-defensive-flag.js +149 -0
- package/dist/scrub-defensive-flag.js.map +1 -0
- package/package.json +2 -1
- package/schemas/enrichment/ability-dsl/effect.schema.json +1 -1
package/dist/cruncher/buffs.d.ts
CHANGED
|
@@ -54,9 +54,19 @@ export type BuffContribution = {
|
|
|
54
54
|
} | {
|
|
55
55
|
type: "extra-keyword";
|
|
56
56
|
keywordRef: WeaponKeywordRef;
|
|
57
|
-
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Feel-no-pain: roll one D6 per unsaved wound at `threshold`+, ignoring the
|
|
60
|
+
* wound on a pass. `scope` controls which wound stream it applies to:
|
|
61
|
+
* - `"all"` (default): every unsaved wound (main + mortal).
|
|
62
|
+
* - `"mortal"`: mortal-wound stream only (e.g. Death Guard 5+ FNP vs
|
|
63
|
+
* mortals). A target may carry both an all-FNP and a mortal-FNP; the
|
|
64
|
+
* engine rolls both against mortals.
|
|
65
|
+
*/
|
|
66
|
+
| {
|
|
58
67
|
type: "feel-no-pain";
|
|
59
68
|
threshold: number;
|
|
69
|
+
scope?: "all" | "mortal";
|
|
60
70
|
} | {
|
|
61
71
|
type: "damage-mod";
|
|
62
72
|
value: number;
|
|
@@ -84,6 +94,28 @@ export type BuffContribution = {
|
|
|
84
94
|
| {
|
|
85
95
|
type: "ap-mod";
|
|
86
96
|
value: number;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Defender-side: subtract `value` from each unsaved damage point (floored at
|
|
100
|
+
* 1 by the engine). Multiple sources do NOT stack in 10e — the largest
|
|
101
|
+
* reduction wins. The corpus also encodes `"half"` and `"to-zero"`
|
|
102
|
+
* reductions; the buff layer only models the additive form because the
|
|
103
|
+
* other two are typically one-use ablation that doesn't fold into the
|
|
104
|
+
* expected-value math cleanly.
|
|
105
|
+
*/
|
|
106
|
+
| {
|
|
107
|
+
type: "damage-reduction";
|
|
108
|
+
value: number;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Defender-side: ability-granted invulnerable save threshold (e.g. a buff
|
|
112
|
+
* that grants a 4+ invuln). Best (lowest) threshold wins; the engine then
|
|
113
|
+
* picks the better of `printed Sv after AP/cover` and `effective invuln`
|
|
114
|
+
* (invuln bypasses both AP and cover).
|
|
115
|
+
*/
|
|
116
|
+
| {
|
|
117
|
+
type: "invulnerable-save";
|
|
118
|
+
threshold: number;
|
|
87
119
|
};
|
|
88
120
|
/** Optional gating; the resolver drops buffs whose gate fails. */
|
|
89
121
|
export type BuffApplicability = {
|
|
@@ -172,10 +204,16 @@ export type ResolvedModifiers = {
|
|
|
172
204
|
keywordRef: WeaponKeywordRef;
|
|
173
205
|
source: BuffSource;
|
|
174
206
|
}[];
|
|
207
|
+
/** All-wound FNP — fires on the main and mortal damage streams alike. */
|
|
175
208
|
feelNoPain: {
|
|
176
209
|
threshold: number;
|
|
177
210
|
dominantSource: BuffSource;
|
|
178
211
|
} | null;
|
|
212
|
+
/** Mortal-only FNP — fires only on the mortal-wound damage stream. */
|
|
213
|
+
feelNoPainMortal: {
|
|
214
|
+
threshold: number;
|
|
215
|
+
dominantSource: BuffSource;
|
|
216
|
+
} | null;
|
|
179
217
|
damageMod: {
|
|
180
218
|
value: number;
|
|
181
219
|
sources: BuffSource[];
|
|
@@ -196,6 +234,24 @@ export type ResolvedModifiers = {
|
|
|
196
234
|
value: number;
|
|
197
235
|
sources: BuffSource[];
|
|
198
236
|
};
|
|
237
|
+
/**
|
|
238
|
+
* Defender-side damage reduction. Highest-wins (multiple sources do not
|
|
239
|
+
* stack in 10e); the dominant source is the one whose value matches the
|
|
240
|
+
* surviving reduction.
|
|
241
|
+
*/
|
|
242
|
+
damageReduction: {
|
|
243
|
+
value: number;
|
|
244
|
+
dominantSource: BuffSource | null;
|
|
245
|
+
};
|
|
246
|
+
/**
|
|
247
|
+
* Ability-granted invulnerable save. Best (lowest) threshold wins. `null`
|
|
248
|
+
* when no ability granted one; the engine still uses the unit's printed
|
|
249
|
+
* `invuln_sv` from the profile in that case.
|
|
250
|
+
*/
|
|
251
|
+
invulnerable: {
|
|
252
|
+
threshold: number;
|
|
253
|
+
dominantSource: BuffSource;
|
|
254
|
+
} | null;
|
|
199
255
|
};
|
|
200
256
|
/**
|
|
201
257
|
* Collapse a flat buff stack into a {@link ResolvedModifiers} read-out. Pure
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buffs.d.ts","sourceRoot":"","sources":["../../src/cruncher/buffs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAE7C,iFAAiF;AACjF,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC/D;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EACP,MAAM,GACN,YAAY,GACZ,sBAAsB,GACtB,MAAM,GACN,UAAU,GACV,SAAS,CAAC;IACd;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GACD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC,oFAAoF;AACpF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC,CAAC;AAEF,iFAAiF;AACjF,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;IAC1C,MAAM,EAAE,MAAM,GAAG,cAAc,CAAC;CACjC,GACD;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,UAAU,EAAE,gBAAgB,CAAA;CAAE
|
|
1
|
+
{"version":3,"file":"buffs.d.ts","sourceRoot":"","sources":["../../src/cruncher/buffs.ts"],"names":[],"mappings":"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"}
|
package/dist/cruncher/buffs.js
CHANGED
|
@@ -50,11 +50,14 @@ export function resolveBuffs(buffs, ctx) {
|
|
|
50
50
|
rerolls: {},
|
|
51
51
|
extraKeywords: [],
|
|
52
52
|
feelNoPain: null,
|
|
53
|
+
feelNoPainMortal: null,
|
|
53
54
|
damageMod: { value: 0, sources: [] },
|
|
54
55
|
attacksMod: { value: 0, sources: [] },
|
|
55
56
|
strengthMod: { value: 0, sources: [] },
|
|
56
57
|
toughnessMod: { value: 0, sources: [] },
|
|
57
58
|
apMod: { value: 0, sources: [] },
|
|
59
|
+
damageReduction: { value: 0, dominantSource: null },
|
|
60
|
+
invulnerable: null,
|
|
58
61
|
};
|
|
59
62
|
// Hit / wound mods: sum, then cap at ±1, with dominant source picked from
|
|
60
63
|
// the contributors whose sign matches the surviving value.
|
|
@@ -100,11 +103,17 @@ export function resolveBuffs(buffs, ctx) {
|
|
|
100
103
|
}
|
|
101
104
|
break;
|
|
102
105
|
}
|
|
103
|
-
case "feel-no-pain":
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
case "feel-no-pain": {
|
|
107
|
+
// Best (lowest) threshold wins per scope. An undeclared scope is
|
|
108
|
+
// treated as "all" — that's the existing convention (unscoped FNP =
|
|
109
|
+
// applies to every wound) and keeps every shipped FNP buff regression-safe.
|
|
110
|
+
const scope = c.scope ?? "all";
|
|
111
|
+
const slot = scope === "mortal" ? "feelNoPainMortal" : "feelNoPain";
|
|
112
|
+
if (out[slot] === null || c.threshold < out[slot].threshold) {
|
|
113
|
+
out[slot] = { threshold: c.threshold, dominantSource: b.source };
|
|
106
114
|
}
|
|
107
115
|
break;
|
|
116
|
+
}
|
|
108
117
|
case "damage-mod":
|
|
109
118
|
out.damageMod.value += c.value;
|
|
110
119
|
out.damageMod.sources.push(b.source);
|
|
@@ -125,6 +134,26 @@ export function resolveBuffs(buffs, ctx) {
|
|
|
125
134
|
out.apMod.value += c.value;
|
|
126
135
|
out.apMod.sources.push(b.source);
|
|
127
136
|
break;
|
|
137
|
+
case "damage-reduction":
|
|
138
|
+
// Highest reduction wins (no stacking). Ties break by source rank so
|
|
139
|
+
// an ability source is preferred over a manual one for provenance
|
|
140
|
+
// purposes; either way the resolved value is unchanged.
|
|
141
|
+
if (out.damageReduction.dominantSource === null ||
|
|
142
|
+
c.value > out.damageReduction.value ||
|
|
143
|
+
(c.value === out.damageReduction.value &&
|
|
144
|
+
rank(b.source) < rank(out.damageReduction.dominantSource))) {
|
|
145
|
+
out.damageReduction = { value: c.value, dominantSource: b.source };
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
case "invulnerable-save":
|
|
149
|
+
// Best (lowest threshold) wins. Same tie-break by source rank.
|
|
150
|
+
if (out.invulnerable === null ||
|
|
151
|
+
c.threshold < out.invulnerable.threshold ||
|
|
152
|
+
(c.threshold === out.invulnerable.threshold &&
|
|
153
|
+
rank(b.source) < rank(out.invulnerable.dominantSource))) {
|
|
154
|
+
out.invulnerable = { threshold: c.threshold, dominantSource: b.source };
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
128
157
|
}
|
|
129
158
|
}
|
|
130
159
|
out.hitMod = capModifier(hitContribs);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buffs.js","sourceRoot":"","sources":["../../src/cruncher/buffs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The flat `Buff` type every contribution flows through, and the\n * {@link resolveBuffs} resolver that collapses a stack into a\n * {@link ResolvedModifiers} read-out the engine can consume.\n *\n * The same shape carries weapon-keyword effects, ability buffs, stratagem\n * effects, and manual UI toggles — reroll-stacking, hit/wound caps, and\n * feel-no-pain-best-threshold all fall out of one resolver rather than each\n * source kind reinventing precedence.\n *\n * @packageDocumentation\n */\nimport type { Phase } from \"../generated.js\";\n\n/** Where a buff originated. Drives stable tie-breaking inside `resolveBuffs`. */\nexport type BuffSource =\n | { kind: \"weapon-keyword\"; weaponId: string; keywordId: string }\n | {\n kind: \"ability\";\n abilityId: string;\n abilityKind:\n | \"army\"\n | \"detachment\"\n | \"detachment-stratagem\"\n | \"unit\"\n | \"attached\"\n | \"support\";\n /**\n * For `abilityKind: \"attached\"`, the combined-unit member the ability\n * came from (so the UI can name it and show its leader/bodyguard role).\n * Absent for other kinds.\n */\n sourceUnitId?: string;\n }\n | { kind: \"manual\"; label: string };\n\n/** A weapon-keyword reference (id + parameter map), as found on weapon profiles. */\nexport type WeaponKeywordRef = {\n keyword_id: string;\n parameters?: Record<string, unknown>;\n};\n\n/** One typed contribution; the engine reads `ResolvedModifiers` for the rest. */\nexport type BuffContribution =\n | { type: \"hit-mod\"; value: number }\n | { type: \"wound-mod\"; value: number }\n | { type: \"save-mod\"; value: number }\n | { type: \"cover\" }\n | {\n type: \"reroll\";\n roll: \"hit\" | \"wound\" | \"save\" | \"damage\";\n subset: \"ones\" | \"all-failures\";\n }\n | { type: \"extra-keyword\"; keywordRef: WeaponKeywordRef }\n | { type: \"feel-no-pain\"; threshold: number }\n | { type: \"damage-mod\"; value: number }\n /** Additive modifier to the attacker's per-model attack count (A stat). */\n | { type: \"attacks-mod\"; value: number }\n /** Additive modifier to the attacker's Strength stat. */\n | { type: \"strength-mod\"; value: number }\n /** Additive modifier to the defender's Toughness stat. */\n | { type: \"toughness-mod\"; value: number }\n /**\n * Additive modifier to the attacker's weapon AP. AP is signed against the\n * defender's save (negative = more piercing), so a value of `-1` here makes\n * the weapon one AP more piercing.\n */\n | { type: \"ap-mod\"; value: number };\n\n/** Optional gating; the resolver drops buffs whose gate fails. */\nexport type BuffApplicability = {\n phases?: Phase[];\n rollType?: \"hit\" | \"wound\" | \"save\" | \"damage\";\n /** Target must carry this keyword (case-insensitive). */\n requiresTargetKeyword?: string;\n /** Attacker must carry this keyword (case-insensitive). */\n requiresAttackerKeyword?: string;\n};\n\n/** A single buff: where it came from, when it applies, what it contributes. */\nexport type Buff = {\n source: BuffSource;\n applicableWhen?: BuffApplicability;\n contribution: BuffContribution;\n};\n\n/**\n * Shared engine context. Carries the phase plus a few attacker/target flags\n * the keyword translator and the resolver both need. The engine fills it from\n * its `EngineInput.context` plus the unit-keyword unions; the resolver reads\n * only the subset relevant to its `applicableWhen` checks.\n */\nexport type EngineContext = {\n phase: Phase;\n /** Attacker has not moved this turn — Heavy fires its +1 to hit. */\n attackerStationary?: boolean;\n /**\n * Attacker made a charge move this turn — drives the `charged-this-turn`\n * condition (e.g. World Eaters' Relentless Rage). Left undefined when the\n * caller can't determine it — the condition then evaluates as `\"unknown\"` and\n * the SPA surfaces a diagnostic (mirrors `attackerStationary` / `timing`).\n */\n attackerCharged?: boolean;\n /** Within half the weapon's range — Melta / Rapid Fire fire. */\n withinHalfRange?: boolean;\n /** Attacker benefits from cover (mostly informational; cover applies to defenders). */\n attackerInCover?: boolean;\n /** Target is in cover — the resolver flips on `cover`, the engine applies +1 to save. */\n targetInCover?: boolean;\n /** Attacker keywords (union of unit.keywords + faction_keywords), lower-cased. */\n attackerKeywords?: ReadonlyArray<string>;\n /** Target keywords (union of unit.keywords + faction_keywords), lower-cased. */\n targetKeywords?: ReadonlyArray<string>;\n /**\n * Sub-phase timing flag (e.g. `\"start-of-phase\"`, `\"end-of-phase\"`,\n * `\"on-destroyed\"`). Consumed by the `timing-is` condition. Left undefined\n * when the caller can't pin a sub-phase down — the condition then evaluates\n * as `\"unknown\"` and the SPA surfaces a diagnostic.\n */\n timing?: string;\n /**\n * The buffed unit is part of a combined (\"attached\") unit — a leader is\n * attached to a bodyguard, or vice-versa. Drives the `is-attached` and\n * `model-is-leader` conditions. Derived from a non-empty\n * `EligibilityInput.attachedUnitIds`. Left undefined when the caller can't\n * determine attachment — the conditions then evaluate as `\"unknown\"` and the\n * SPA surfaces a diagnostic (mirrors how `timing` undefined behaves).\n */\n attackerAttached?: boolean;\n};\n\n/** Back-compat alias — `resolveBuffs` accepts the shared engine context. */\nexport type ResolveContext = EngineContext;\n\n/** Read-out of a resolved buff stack, with provenance per field. */\nexport type ResolvedModifiers = {\n hitMod: { value: number; dominantSource: BuffSource | null };\n woundMod: { value: number; dominantSource: BuffSource | null };\n saveMod: { value: number; sources: BuffSource[] };\n cover: { active: boolean; source: BuffSource | null };\n rerolls: Partial<\n Record<\n \"hit\" | \"wound\" | \"save\" | \"damage\",\n { subset: \"ones\" | \"all-failures\"; dominantSource: BuffSource }\n >\n >;\n extraKeywords: { keywordRef: WeaponKeywordRef; source: BuffSource }[];\n feelNoPain: { threshold: number; dominantSource: BuffSource } | null;\n damageMod: { value: number; sources: BuffSource[] };\n attacksMod: { value: number; sources: BuffSource[] };\n strengthMod: { value: number; sources: BuffSource[] };\n toughnessMod: { value: number; sources: BuffSource[] };\n apMod: { value: number; sources: BuffSource[] };\n};\n\n/** Stable ordering used to break ties when multiple buffs claim the same field. */\nconst SOURCE_KIND_RANK: Record<string, number> = {\n \"ability:army\": 0,\n \"ability:detachment\": 1,\n \"ability:detachment-stratagem\": 2,\n \"ability:unit\": 3,\n \"ability:attached\": 4,\n \"ability:support\": 5,\n manual: 6,\n \"weapon-keyword\": 7,\n};\n\nfunction rank(s: BuffSource): number {\n if (s.kind === \"ability\") return SOURCE_KIND_RANK[`ability:${s.abilityKind}`] ?? 99;\n return SOURCE_KIND_RANK[s.kind] ?? 99;\n}\n\nfunction applies(buff: Buff, ctx: ResolveContext): boolean {\n const w = buff.applicableWhen;\n if (!w) return true;\n if (w.phases && w.phases.length > 0 && !w.phases.includes(ctx.phase)) return false;\n if (w.rollType && buff.contribution.type === \"reroll\" && buff.contribution.roll !== w.rollType) {\n return false;\n }\n if (w.requiresTargetKeyword) {\n const target = ctx.targetKeywords ?? [];\n if (!target.includes(w.requiresTargetKeyword.toLowerCase())) return false;\n }\n if (w.requiresAttackerKeyword) {\n const attacker = ctx.attackerKeywords ?? [];\n if (!attacker.includes(w.requiresAttackerKeyword.toLowerCase())) return false;\n }\n return true;\n}\n\n/**\n * Collapse a flat buff stack into a {@link ResolvedModifiers} read-out. Pure\n * function; the engine — and any UI that wants to render the resolved table\n * before crunching — both go through this.\n */\nexport function resolveBuffs(buffs: Buff[], ctx: ResolveContext): ResolvedModifiers {\n const live = buffs.filter((b) => applies(b, ctx));\n\n const out: ResolvedModifiers = {\n hitMod: { value: 0, dominantSource: null },\n woundMod: { value: 0, dominantSource: null },\n saveMod: { value: 0, sources: [] },\n cover: { active: false, source: null },\n rerolls: {},\n extraKeywords: [],\n feelNoPain: null,\n damageMod: { value: 0, sources: [] },\n attacksMod: { value: 0, sources: [] },\n strengthMod: { value: 0, sources: [] },\n toughnessMod: { value: 0, sources: [] },\n apMod: { value: 0, sources: [] },\n };\n\n // Hit / wound mods: sum, then cap at ±1, with dominant source picked from\n // the contributors whose sign matches the surviving value.\n const hitContribs: { value: number; source: BuffSource }[] = [];\n const woundContribs: { value: number; source: BuffSource }[] = [];\n\n for (const b of live) {\n const c = b.contribution;\n switch (c.type) {\n case \"hit-mod\":\n hitContribs.push({ value: c.value, source: b.source });\n break;\n case \"wound-mod\":\n woundContribs.push({ value: c.value, source: b.source });\n break;\n case \"save-mod\":\n out.saveMod.value += c.value;\n out.saveMod.sources.push(b.source);\n break;\n case \"cover\":\n if (!out.cover.active || rank(b.source) < rank(out.cover.source!)) {\n out.cover = { active: true, source: b.source };\n }\n break;\n case \"reroll\": {\n const cur = out.rerolls[c.roll];\n const incoming = c.subset;\n if (!cur) {\n out.rerolls[c.roll] = { subset: incoming, dominantSource: b.source };\n } else {\n const incomingStronger =\n (incoming === \"all-failures\" && cur.subset === \"ones\") ||\n (incoming === cur.subset && rank(b.source) < rank(cur.dominantSource));\n if (incomingStronger) {\n out.rerolls[c.roll] = { subset: incoming, dominantSource: b.source };\n }\n }\n break;\n }\n case \"extra-keyword\": {\n const key = `${c.keywordRef.keyword_id}::${JSON.stringify(c.keywordRef.parameters ?? {})}`;\n if (!out.extraKeywords.some((e) => keyOf(e.keywordRef) === key)) {\n out.extraKeywords.push({ keywordRef: c.keywordRef, source: b.source });\n }\n break;\n }\n case \"feel-no-pain\":\n if (out.feelNoPain === null || c.threshold < out.feelNoPain.threshold) {\n out.feelNoPain = { threshold: c.threshold, dominantSource: b.source };\n }\n break;\n case \"damage-mod\":\n out.damageMod.value += c.value;\n out.damageMod.sources.push(b.source);\n break;\n case \"attacks-mod\":\n out.attacksMod.value += c.value;\n out.attacksMod.sources.push(b.source);\n break;\n case \"strength-mod\":\n out.strengthMod.value += c.value;\n out.strengthMod.sources.push(b.source);\n break;\n case \"toughness-mod\":\n out.toughnessMod.value += c.value;\n out.toughnessMod.sources.push(b.source);\n break;\n case \"ap-mod\":\n out.apMod.value += c.value;\n out.apMod.sources.push(b.source);\n break;\n }\n }\n\n out.hitMod = capModifier(hitContribs);\n out.woundMod = capModifier(woundContribs);\n\n return out;\n}\n\nfunction keyOf(ref: WeaponKeywordRef): string {\n return `${ref.keyword_id}::${JSON.stringify(ref.parameters ?? {})}`;\n}\n\n/** Sum, clamp to ±1, then pick the dominant contributing source by rank. */\nfunction capModifier(\n contribs: { value: number; source: BuffSource }[],\n): { value: number; dominantSource: BuffSource | null } {\n if (contribs.length === 0) return { value: 0, dominantSource: null };\n const sum = contribs.reduce((a, c) => a + c.value, 0);\n const capped = Math.max(-1, Math.min(1, sum));\n if (capped === 0) return { value: 0, dominantSource: null };\n const sign = Math.sign(capped);\n const matching = contribs.filter((c) => Math.sign(c.value) === sign);\n matching.sort((a, b) => rank(a.source) - rank(b.source));\n return { value: capped, dominantSource: matching[0]?.source ?? null };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"buffs.js","sourceRoot":"","sources":["../../src/cruncher/buffs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The flat `Buff` type every contribution flows through, and the\n * {@link resolveBuffs} resolver that collapses a stack into a\n * {@link ResolvedModifiers} read-out the engine can consume.\n *\n * The same shape carries weapon-keyword effects, ability buffs, stratagem\n * effects, and manual UI toggles — reroll-stacking, hit/wound caps, and\n * feel-no-pain-best-threshold all fall out of one resolver rather than each\n * source kind reinventing precedence.\n *\n * @packageDocumentation\n */\nimport type { Phase } from \"../generated.js\";\n\n/** Where a buff originated. Drives stable tie-breaking inside `resolveBuffs`. */\nexport type BuffSource =\n | { kind: \"weapon-keyword\"; weaponId: string; keywordId: string }\n | {\n kind: \"ability\";\n abilityId: string;\n abilityKind:\n | \"army\"\n | \"detachment\"\n | \"detachment-stratagem\"\n | \"unit\"\n | \"attached\"\n | \"support\";\n /**\n * For `abilityKind: \"attached\"`, the combined-unit member the ability\n * came from (so the UI can name it and show its leader/bodyguard role).\n * Absent for other kinds.\n */\n sourceUnitId?: string;\n }\n | { kind: \"manual\"; label: string };\n\n/** A weapon-keyword reference (id + parameter map), as found on weapon profiles. */\nexport type WeaponKeywordRef = {\n keyword_id: string;\n parameters?: Record<string, unknown>;\n};\n\n/** One typed contribution; the engine reads `ResolvedModifiers` for the rest. */\nexport type BuffContribution =\n | { type: \"hit-mod\"; value: number }\n | { type: \"wound-mod\"; value: number }\n | { type: \"save-mod\"; value: number }\n | { type: \"cover\" }\n | {\n type: \"reroll\";\n roll: \"hit\" | \"wound\" | \"save\" | \"damage\";\n subset: \"ones\" | \"all-failures\";\n }\n | { type: \"extra-keyword\"; keywordRef: WeaponKeywordRef }\n /**\n * Feel-no-pain: roll one D6 per unsaved wound at `threshold`+, ignoring the\n * wound on a pass. `scope` controls which wound stream it applies to:\n * - `\"all\"` (default): every unsaved wound (main + mortal).\n * - `\"mortal\"`: mortal-wound stream only (e.g. Death Guard 5+ FNP vs\n * mortals). A target may carry both an all-FNP and a mortal-FNP; the\n * engine rolls both against mortals.\n */\n | { type: \"feel-no-pain\"; threshold: number; scope?: \"all\" | \"mortal\" }\n | { type: \"damage-mod\"; value: number }\n /** Additive modifier to the attacker's per-model attack count (A stat). */\n | { type: \"attacks-mod\"; value: number }\n /** Additive modifier to the attacker's Strength stat. */\n | { type: \"strength-mod\"; value: number }\n /** Additive modifier to the defender's Toughness stat. */\n | { type: \"toughness-mod\"; value: number }\n /**\n * Additive modifier to the attacker's weapon AP. AP is signed against the\n * defender's save (negative = more piercing), so a value of `-1` here makes\n * the weapon one AP more piercing.\n */\n | { type: \"ap-mod\"; value: number }\n /**\n * Defender-side: subtract `value` from each unsaved damage point (floored at\n * 1 by the engine). Multiple sources do NOT stack in 10e — the largest\n * reduction wins. The corpus also encodes `\"half\"` and `\"to-zero\"`\n * reductions; the buff layer only models the additive form because the\n * other two are typically one-use ablation that doesn't fold into the\n * expected-value math cleanly.\n */\n | { type: \"damage-reduction\"; value: number }\n /**\n * Defender-side: ability-granted invulnerable save threshold (e.g. a buff\n * that grants a 4+ invuln). Best (lowest) threshold wins; the engine then\n * picks the better of `printed Sv after AP/cover` and `effective invuln`\n * (invuln bypasses both AP and cover).\n */\n | { type: \"invulnerable-save\"; threshold: number };\n\n/** Optional gating; the resolver drops buffs whose gate fails. */\nexport type BuffApplicability = {\n phases?: Phase[];\n rollType?: \"hit\" | \"wound\" | \"save\" | \"damage\";\n /** Target must carry this keyword (case-insensitive). */\n requiresTargetKeyword?: string;\n /** Attacker must carry this keyword (case-insensitive). */\n requiresAttackerKeyword?: string;\n};\n\n/** A single buff: where it came from, when it applies, what it contributes. */\nexport type Buff = {\n source: BuffSource;\n applicableWhen?: BuffApplicability;\n contribution: BuffContribution;\n};\n\n/**\n * Shared engine context. Carries the phase plus a few attacker/target flags\n * the keyword translator and the resolver both need. The engine fills it from\n * its `EngineInput.context` plus the unit-keyword unions; the resolver reads\n * only the subset relevant to its `applicableWhen` checks.\n */\nexport type EngineContext = {\n phase: Phase;\n /** Attacker has not moved this turn — Heavy fires its +1 to hit. */\n attackerStationary?: boolean;\n /**\n * Attacker made a charge move this turn — drives the `charged-this-turn`\n * condition (e.g. World Eaters' Relentless Rage). Left undefined when the\n * caller can't determine it — the condition then evaluates as `\"unknown\"` and\n * the SPA surfaces a diagnostic (mirrors `attackerStationary` / `timing`).\n */\n attackerCharged?: boolean;\n /** Within half the weapon's range — Melta / Rapid Fire fire. */\n withinHalfRange?: boolean;\n /** Attacker benefits from cover (mostly informational; cover applies to defenders). */\n attackerInCover?: boolean;\n /** Target is in cover — the resolver flips on `cover`, the engine applies +1 to save. */\n targetInCover?: boolean;\n /** Attacker keywords (union of unit.keywords + faction_keywords), lower-cased. */\n attackerKeywords?: ReadonlyArray<string>;\n /** Target keywords (union of unit.keywords + faction_keywords), lower-cased. */\n targetKeywords?: ReadonlyArray<string>;\n /**\n * Sub-phase timing flag (e.g. `\"start-of-phase\"`, `\"end-of-phase\"`,\n * `\"on-destroyed\"`). Consumed by the `timing-is` condition. Left undefined\n * when the caller can't pin a sub-phase down — the condition then evaluates\n * as `\"unknown\"` and the SPA surfaces a diagnostic.\n */\n timing?: string;\n /**\n * The buffed unit is part of a combined (\"attached\") unit — a leader is\n * attached to a bodyguard, or vice-versa. Drives the `is-attached` and\n * `model-is-leader` conditions. Derived from a non-empty\n * `EligibilityInput.attachedUnitIds`. Left undefined when the caller can't\n * determine attachment — the conditions then evaluate as `\"unknown\"` and the\n * SPA surfaces a diagnostic (mirrors how `timing` undefined behaves).\n */\n attackerAttached?: boolean;\n};\n\n/** Back-compat alias — `resolveBuffs` accepts the shared engine context. */\nexport type ResolveContext = EngineContext;\n\n/** Read-out of a resolved buff stack, with provenance per field. */\nexport type ResolvedModifiers = {\n hitMod: { value: number; dominantSource: BuffSource | null };\n woundMod: { value: number; dominantSource: BuffSource | null };\n saveMod: { value: number; sources: BuffSource[] };\n cover: { active: boolean; source: BuffSource | null };\n rerolls: Partial<\n Record<\n \"hit\" | \"wound\" | \"save\" | \"damage\",\n { subset: \"ones\" | \"all-failures\"; dominantSource: BuffSource }\n >\n >;\n extraKeywords: { keywordRef: WeaponKeywordRef; source: BuffSource }[];\n /** All-wound FNP — fires on the main and mortal damage streams alike. */\n feelNoPain: { threshold: number; dominantSource: BuffSource } | null;\n /** Mortal-only FNP — fires only on the mortal-wound damage stream. */\n feelNoPainMortal: { threshold: number; dominantSource: BuffSource } | null;\n damageMod: { value: number; sources: BuffSource[] };\n attacksMod: { value: number; sources: BuffSource[] };\n strengthMod: { value: number; sources: BuffSource[] };\n toughnessMod: { value: number; sources: BuffSource[] };\n apMod: { value: number; sources: BuffSource[] };\n /**\n * Defender-side damage reduction. Highest-wins (multiple sources do not\n * stack in 10e); the dominant source is the one whose value matches the\n * surviving reduction.\n */\n damageReduction: { value: number; dominantSource: BuffSource | null };\n /**\n * Ability-granted invulnerable save. Best (lowest) threshold wins. `null`\n * when no ability granted one; the engine still uses the unit's printed\n * `invuln_sv` from the profile in that case.\n */\n invulnerable: { threshold: number; dominantSource: BuffSource } | null;\n};\n\n/** Stable ordering used to break ties when multiple buffs claim the same field. */\nconst SOURCE_KIND_RANK: Record<string, number> = {\n \"ability:army\": 0,\n \"ability:detachment\": 1,\n \"ability:detachment-stratagem\": 2,\n \"ability:unit\": 3,\n \"ability:attached\": 4,\n \"ability:support\": 5,\n manual: 6,\n \"weapon-keyword\": 7,\n};\n\nfunction rank(s: BuffSource): number {\n if (s.kind === \"ability\") return SOURCE_KIND_RANK[`ability:${s.abilityKind}`] ?? 99;\n return SOURCE_KIND_RANK[s.kind] ?? 99;\n}\n\nfunction applies(buff: Buff, ctx: ResolveContext): boolean {\n const w = buff.applicableWhen;\n if (!w) return true;\n if (w.phases && w.phases.length > 0 && !w.phases.includes(ctx.phase)) return false;\n if (w.rollType && buff.contribution.type === \"reroll\" && buff.contribution.roll !== w.rollType) {\n return false;\n }\n if (w.requiresTargetKeyword) {\n const target = ctx.targetKeywords ?? [];\n if (!target.includes(w.requiresTargetKeyword.toLowerCase())) return false;\n }\n if (w.requiresAttackerKeyword) {\n const attacker = ctx.attackerKeywords ?? [];\n if (!attacker.includes(w.requiresAttackerKeyword.toLowerCase())) return false;\n }\n return true;\n}\n\n/**\n * Collapse a flat buff stack into a {@link ResolvedModifiers} read-out. Pure\n * function; the engine — and any UI that wants to render the resolved table\n * before crunching — both go through this.\n */\nexport function resolveBuffs(buffs: Buff[], ctx: ResolveContext): ResolvedModifiers {\n const live = buffs.filter((b) => applies(b, ctx));\n\n const out: ResolvedModifiers = {\n hitMod: { value: 0, dominantSource: null },\n woundMod: { value: 0, dominantSource: null },\n saveMod: { value: 0, sources: [] },\n cover: { active: false, source: null },\n rerolls: {},\n extraKeywords: [],\n feelNoPain: null,\n feelNoPainMortal: null,\n damageMod: { value: 0, sources: [] },\n attacksMod: { value: 0, sources: [] },\n strengthMod: { value: 0, sources: [] },\n toughnessMod: { value: 0, sources: [] },\n apMod: { value: 0, sources: [] },\n damageReduction: { value: 0, dominantSource: null },\n invulnerable: null,\n };\n\n // Hit / wound mods: sum, then cap at ±1, with dominant source picked from\n // the contributors whose sign matches the surviving value.\n const hitContribs: { value: number; source: BuffSource }[] = [];\n const woundContribs: { value: number; source: BuffSource }[] = [];\n\n for (const b of live) {\n const c = b.contribution;\n switch (c.type) {\n case \"hit-mod\":\n hitContribs.push({ value: c.value, source: b.source });\n break;\n case \"wound-mod\":\n woundContribs.push({ value: c.value, source: b.source });\n break;\n case \"save-mod\":\n out.saveMod.value += c.value;\n out.saveMod.sources.push(b.source);\n break;\n case \"cover\":\n if (!out.cover.active || rank(b.source) < rank(out.cover.source!)) {\n out.cover = { active: true, source: b.source };\n }\n break;\n case \"reroll\": {\n const cur = out.rerolls[c.roll];\n const incoming = c.subset;\n if (!cur) {\n out.rerolls[c.roll] = { subset: incoming, dominantSource: b.source };\n } else {\n const incomingStronger =\n (incoming === \"all-failures\" && cur.subset === \"ones\") ||\n (incoming === cur.subset && rank(b.source) < rank(cur.dominantSource));\n if (incomingStronger) {\n out.rerolls[c.roll] = { subset: incoming, dominantSource: b.source };\n }\n }\n break;\n }\n case \"extra-keyword\": {\n const key = `${c.keywordRef.keyword_id}::${JSON.stringify(c.keywordRef.parameters ?? {})}`;\n if (!out.extraKeywords.some((e) => keyOf(e.keywordRef) === key)) {\n out.extraKeywords.push({ keywordRef: c.keywordRef, source: b.source });\n }\n break;\n }\n case \"feel-no-pain\": {\n // Best (lowest) threshold wins per scope. An undeclared scope is\n // treated as \"all\" — that's the existing convention (unscoped FNP =\n // applies to every wound) and keeps every shipped FNP buff regression-safe.\n const scope = c.scope ?? \"all\";\n const slot = scope === \"mortal\" ? \"feelNoPainMortal\" : \"feelNoPain\";\n if (out[slot] === null || c.threshold < out[slot]!.threshold) {\n out[slot] = { threshold: c.threshold, dominantSource: b.source };\n }\n break;\n }\n case \"damage-mod\":\n out.damageMod.value += c.value;\n out.damageMod.sources.push(b.source);\n break;\n case \"attacks-mod\":\n out.attacksMod.value += c.value;\n out.attacksMod.sources.push(b.source);\n break;\n case \"strength-mod\":\n out.strengthMod.value += c.value;\n out.strengthMod.sources.push(b.source);\n break;\n case \"toughness-mod\":\n out.toughnessMod.value += c.value;\n out.toughnessMod.sources.push(b.source);\n break;\n case \"ap-mod\":\n out.apMod.value += c.value;\n out.apMod.sources.push(b.source);\n break;\n case \"damage-reduction\":\n // Highest reduction wins (no stacking). Ties break by source rank so\n // an ability source is preferred over a manual one for provenance\n // purposes; either way the resolved value is unchanged.\n if (\n out.damageReduction.dominantSource === null ||\n c.value > out.damageReduction.value ||\n (c.value === out.damageReduction.value &&\n rank(b.source) < rank(out.damageReduction.dominantSource))\n ) {\n out.damageReduction = { value: c.value, dominantSource: b.source };\n }\n break;\n case \"invulnerable-save\":\n // Best (lowest threshold) wins. Same tie-break by source rank.\n if (\n out.invulnerable === null ||\n c.threshold < out.invulnerable.threshold ||\n (c.threshold === out.invulnerable.threshold &&\n rank(b.source) < rank(out.invulnerable.dominantSource))\n ) {\n out.invulnerable = { threshold: c.threshold, dominantSource: b.source };\n }\n break;\n }\n }\n\n out.hitMod = capModifier(hitContribs);\n out.woundMod = capModifier(woundContribs);\n\n return out;\n}\n\nfunction keyOf(ref: WeaponKeywordRef): string {\n return `${ref.keyword_id}::${JSON.stringify(ref.parameters ?? {})}`;\n}\n\n/** Sum, clamp to ±1, then pick the dominant contributing source by rank. */\nfunction capModifier(\n contribs: { value: number; source: BuffSource }[],\n): { value: number; dominantSource: BuffSource | null } {\n if (contribs.length === 0) return { value: 0, dominantSource: null };\n const sum = contribs.reduce((a, c) => a + c.value, 0);\n const capped = Math.max(-1, Math.min(1, sum));\n if (capped === 0) return { value: 0, dominantSource: null };\n const sign = Math.sign(capped);\n const matching = contribs.filter((c) => Math.sign(c.value) === sign);\n matching.sort((a, b) => rank(a.source) - rank(b.source));\n return { value: capped, dominantSource: matching[0]?.source ?? null };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/cruncher/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EACL,KAAK,IAAI,EACT,KAAK,aAAa,EAElB,KAAK,iBAAiB,EAEvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAE7C,MAAM,MAAM,gBAAgB,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC;AACxE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,IAAI,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,KAAK,GAAG;IAClB,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,WAAW,GAAG,eAAe,CAAC;IAC3F,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,MAAM,EAAE,gBAAgB,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,OAAO,EAAE,aAAa,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAAC,QAAQ,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAE5E;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,YAAY,
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/cruncher/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EACL,KAAK,IAAI,EACT,KAAK,aAAa,EAElB,KAAK,iBAAiB,EAEvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAE7C,MAAM,MAAM,gBAAgB,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC;AACxE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,IAAI,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,KAAK,GAAG;IAClB,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,WAAW,GAAG,eAAe,CAAC;IAC3F,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,MAAM,EAAE,gBAAgB,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,OAAO,EAAE,aAAa,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAAC,QAAQ,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAE5E;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,YAAY,CA2M1E;AA0LD,YAAY,EAAE,KAAK,EAAE,CAAC"}
|
package/dist/cruncher/engine.js
CHANGED
|
@@ -121,8 +121,16 @@ export function crunch(input, dataset) {
|
|
|
121
121
|
const covered = resolved.cover.active && !ignoresCover && input.attacker.weapon.type === "ranged";
|
|
122
122
|
const armorAfterCover = covered ? Math.max(3, armorTargetRaw - 1) : armorTargetRaw;
|
|
123
123
|
const armorFinal = clamp(armorAfterCover, 2, 7);
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
// The unit's printed invuln (from the profile) and any ability-granted
|
|
125
|
+
// invuln combine best-wins (lowest threshold). Invuln bypasses AP and cover
|
|
126
|
+
// — only the armor branch above is affected by those — so the final save is
|
|
127
|
+
// min(armor-after-AP-and-cover, effective-invuln).
|
|
128
|
+
const printedInvuln = unitProfile.invuln_sv ?? null;
|
|
129
|
+
const abilityInvuln = resolved.invulnerable?.threshold ?? null;
|
|
130
|
+
const effectiveInvuln = printedInvuln !== null && abilityInvuln !== null
|
|
131
|
+
? Math.min(printedInvuln, abilityInvuln)
|
|
132
|
+
: (printedInvuln ?? abilityInvuln);
|
|
133
|
+
const effectiveSaveTarget = effectiveInvuln !== null ? Math.min(armorFinal, effectiveInvuln) : armorFinal;
|
|
126
134
|
const saveProbs = checkProbabilities({
|
|
127
135
|
unmodifiedNeeded: effectiveSaveTarget,
|
|
128
136
|
modifier: 0,
|
|
@@ -136,32 +144,38 @@ export function crunch(input, dataset) {
|
|
|
136
144
|
stages.push({
|
|
137
145
|
name: "unsaved",
|
|
138
146
|
expected: unsaved,
|
|
139
|
-
detail: `Sv${unitProfile.Sv}+, AP${signed(AP)}${apMod !== 0 ? ` (apmod ${signed(apMod)})` : ""}${saveMod !== 0 ? `, savemod ${signed(saveMod)}` : ""}${covered ? ", cover (+1, cap 3+)" : ""} → effective ${effectiveSaveTarget}+ (P(save)=${pSaved.toFixed(4)})`,
|
|
147
|
+
detail: `Sv${unitProfile.Sv}+, AP${signed(AP)}${apMod !== 0 ? ` (apmod ${signed(apMod)})` : ""}${saveMod !== 0 ? `, savemod ${signed(saveMod)}` : ""}${covered ? ", cover (+1, cap 3+)" : ""}${abilityInvuln !== null ? `, invuln ${abilityInvuln}+ (ability)` : ""} → effective ${effectiveSaveTarget}+ (P(save)=${pSaved.toFixed(4)})`,
|
|
140
148
|
});
|
|
141
149
|
// 5. Damage
|
|
142
150
|
const baseD = evalStatValue(weaponProfile.stats.D);
|
|
143
151
|
const melta = findKeyword(resolved, "melta");
|
|
144
152
|
const meltaBonus = melta && halfRange ? evalStatValue(melta.parameters?.value) : 0;
|
|
145
|
-
const
|
|
153
|
+
const beforeReduction = Math.max(0, baseD + meltaBonus + resolved.damageMod.value);
|
|
154
|
+
const damageReduction = resolved.damageReduction.value;
|
|
155
|
+
// 10e damage-reduction abilities always carry the canonical "to a minimum
|
|
156
|
+
// of 1" clause, so the floor lives in the math, not the data. The clause
|
|
157
|
+
// only applies when damage-reduction is active — without it, a D1 weapon
|
|
158
|
+
// with a -1 attacker damage-mod still produces 0 damage.
|
|
159
|
+
const damagePerHit = damageReduction > 0 ? Math.max(1, beforeReduction - damageReduction) : beforeReduction;
|
|
146
160
|
const damageMain = unsaved * damagePerHit;
|
|
147
161
|
const damageMortal = mortalWoundsStream * damagePerHit;
|
|
148
162
|
const damage = damageMain + damageMortal;
|
|
149
163
|
stages.push({
|
|
150
164
|
name: "damage",
|
|
151
165
|
expected: damage,
|
|
152
|
-
detail: `D ${baseD}${meltaBonus ? ` + Melta ${meltaBonus} (half range)` : ""}${resolved.damageMod.value !== 0 ? ` ${signed(resolved.damageMod.value)} (mod)` : ""} = ${damagePerHit} per hit; main ${damageMain.toFixed(4)}, mortal ${damageMortal.toFixed(4)}`,
|
|
166
|
+
detail: `D ${baseD}${meltaBonus ? ` + Melta ${meltaBonus} (half range)` : ""}${resolved.damageMod.value !== 0 ? ` ${signed(resolved.damageMod.value)} (mod)` : ""}${damageReduction > 0 ? ` -${damageReduction} (defender, min 1)` : ""} = ${damagePerHit} per hit; main ${damageMain.toFixed(4)}, mortal ${damageMortal.toFixed(4)}`,
|
|
153
167
|
});
|
|
154
168
|
// 6. FNP
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
169
|
+
// Two scopes compose: an all-FNP fires on every unsaved wound; a mortal-FNP
|
|
170
|
+
// fires only on the mortal-wound stream (e.g. Death Guard 5+ FNP vs mortals).
|
|
171
|
+
// A target carrying both rolls both against mortals — independent Bernoulli
|
|
172
|
+
// trials, so the surviving fractions multiply.
|
|
173
|
+
const pSurviveAll = fnpSurvivalFraction(resolved.feelNoPain);
|
|
174
|
+
const pSurviveMortal = fnpSurvivalFraction(resolved.feelNoPainMortal);
|
|
175
|
+
const afterMain = damageMain * pSurviveAll;
|
|
176
|
+
const afterMortal = damageMortal * pSurviveAll * pSurviveMortal;
|
|
177
|
+
const afterFnp = afterMain + afterMortal;
|
|
178
|
+
const fnpDetail = describeFnp(resolved.feelNoPain, resolved.feelNoPainMortal);
|
|
165
179
|
stages.push({ name: "after-fnp", expected: afterFnp, detail: fnpDetail });
|
|
166
180
|
// 7. Models killed
|
|
167
181
|
const W = unitProfile.W;
|
|
@@ -295,6 +309,27 @@ function signed(n) {
|
|
|
295
309
|
return `${n}`;
|
|
296
310
|
return "0";
|
|
297
311
|
}
|
|
312
|
+
/** Fraction of damage that survives a single FNP roll (1 if no FNP). */
|
|
313
|
+
function fnpSurvivalFraction(fnp) {
|
|
314
|
+
if (!fnp)
|
|
315
|
+
return 1;
|
|
316
|
+
const pSucc = Math.max(0, Math.min(1, (7 - fnp.threshold) / 6));
|
|
317
|
+
return 1 - pSucc;
|
|
318
|
+
}
|
|
319
|
+
function describeFnp(all, mortal) {
|
|
320
|
+
if (!all && !mortal)
|
|
321
|
+
return "no FNP";
|
|
322
|
+
const parts = [];
|
|
323
|
+
if (all) {
|
|
324
|
+
const pSucc = (7 - all.threshold) / 6;
|
|
325
|
+
parts.push(`FNP ${all.threshold}+ (P=${pSucc.toFixed(4)})`);
|
|
326
|
+
}
|
|
327
|
+
if (mortal) {
|
|
328
|
+
const pSucc = (7 - mortal.threshold) / 6;
|
|
329
|
+
parts.push(`FNP ${mortal.threshold}+ vs mortals (P=${pSucc.toFixed(4)})`);
|
|
330
|
+
}
|
|
331
|
+
return parts.join(", ");
|
|
332
|
+
}
|
|
298
333
|
function attacksDetail(models, per, rapidFire, blast) {
|
|
299
334
|
const parts = [`${models} × ${per}`];
|
|
300
335
|
if (rapidFire)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/cruncher/engine.ts"],"names":[],"mappings":"AAaA,OAAO,EAGL,YAAY,GAGb,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AA2B7C;;;;GAIG;AACH,MAAM,UAAU,MAAM,CAAC,KAAkB,EAAE,OAAiB;IAC1D,MAAM,EAAE,GAAG,OAAO,IAAI,mBAAmB,EAAE,CAAC;IAC5C,MAAM,aAAa,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IAClF,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,MAAM,IAAI,UAAU,CAClB,iCAAiC,KAAK,CAAC,QAAQ,CAAC,YAAY,+BAA+B,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,EAAE,CACtH,CAAC;IACJ,CAAC;IACD,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IAC1E,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,MAAM,IAAI,UAAU,CAClB,+BAA+B,KAAK,CAAC,MAAM,CAAC,YAAY,6BAA6B,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,CAC5G,CAAC;IACJ,CAAC;IAED,MAAM,cAAc,GAAG,iBAAiB,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC5D,MAAM,GAAG,GAAkB;QACzB,GAAG,KAAK,CAAC,OAAO;QAChB,cAAc,EAAE,KAAK,CAAC,OAAO,CAAC,cAAc,IAAI,cAAc;KAC/D,CAAC;IAEF,0EAA0E;IAC1E,uEAAuE;IACvE,MAAM,YAAY,GAAG,eAAe,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;IAC9D,MAAM,QAAQ,GAAG,YAAY,CAAC,CAAC,GAAG,YAAY,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC;IAEtE,MAAM,MAAM,GAAY,EAAE,CAAC;IAE3B,aAAa;IACb,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,OAAO,CAAC;IACvD,MAAM,KAAK,GAAG,aAAa,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACnD,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC;IAC1D,MAAM,SAAS,GAAG,WAAW,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IACtD,MAAM,SAAS,GAAG,GAAG,CAAC,eAAe,KAAK,IAAI,CAAC;IAC/C,MAAM,sBAAsB,GAAG,SAAS,IAAI,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,SAAS,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACvG,MAAM,KAAK,GAAG,WAAW,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC7C,MAAM,gBAAgB,GAAG,KAAK,CAAC,MAAM,CAAC,UAAU,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,CAAC;IAC5F,MAAM,kBAAkB,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,gBAAgB,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACxE,MAAM,OAAO,GAAG,KAAK,CAAC,YAAY,GAAG,CAAC,eAAe,GAAG,sBAAsB,GAAG,kBAAkB,CAAC,CAAC;IACrG,MAAM,CAAC,IAAI,CAAC;QACV,IAAI,EAAE,SAAS;QACf,QAAQ,EAAE,OAAO;QACjB,MAAM,EAAE,aAAa,CAAC,KAAK,CAAC,YAAY,EAAE,eAAe,EAAE,sBAAsB,EAAE,kBAAkB,CAAC;KACvG,CAAC,CAAC;IAEH,UAAU;IACV,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;IAC1E,MAAM,OAAO,GAAG,CAAC,CAAC,WAAW,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IACnD,IAAI,IAAY,CAAC;IACjB,IAAI,QAAgB,CAAC;IACrB,IAAI,UAAkB,CAAC;IACvB,IAAI,OAAO,EAAE,CAAC;QACZ,IAAI,GAAG,OAAO,CAAC;QACf,QAAQ,GAAG,CAAC,CAAC;QACb,UAAU,GAAG,uBAAuB,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IAC5D,CAAC;SAAM,CAAC;QACN,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;YAChC,MAAM,IAAI,KAAK,CACb,kBAAkB,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,KAAK,CAAC,QAAQ,CAAC,YAAY,YAAY,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CACrH,CAAC;QACJ,CAAC;QACD,MAAM,KAAK,GAAG,kBAAkB,CAAC;YAC/B,gBAAgB,EAAE,OAAO;YACzB,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,KAAK;YAC/B,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,IAAI,MAAM;YAC9C,aAAa,EAAE,IAAI;YACnB,aAAa,EAAE,IAAI;YACnB,aAAa,EAAE,CAAC;SACjB,CAAC,CAAC;QACH,IAAI,GAAG,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC;QAC5B,QAAQ,GAAG,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC;QAChC,UAAU,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,OAAO,UAAU,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,IAAI,MAAM,cAAc,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAClN,CAAC;IACD,MAAM,SAAS,GAAG,WAAW,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;IAC1D,IAAI,SAAS,EAAE,CAAC;QACd,IAAI,IAAI,QAAQ,GAAG,aAAa,CAAC,SAAS,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QAC9D,UAAU,IAAI,qBAAqB,SAAS,CAAC,UAAU,EAAE,KAAK,IAAI,CAAC,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC;IACxG,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;IAElE,YAAY;IACZ,MAAM,CAAC,GAAG,aAAa,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,WAAW,CAAC,KAAK,CAAC;IAC5E,MAAM,CAAC,GAAG,WAAW,CAAC,CAAC,GAAG,QAAQ,CAAC,YAAY,CAAC,KAAK,CAAC;IACtD,MAAM,cAAc,GAAG,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5C,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC3C,IAAI,aAAa,GAAG,CAAC,CAAC,CAAC,cAAc;IACrC,IAAI,IAAI,EAAE,CAAC;QACT,MAAM,QAAQ,GAAI,IAAI,CAAC,UAAU,EAAE,cAAqC,EAAE,WAAW,EAAE,CAAC;QACxF,IAAI,QAAQ,IAAI,cAAc,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YAClD,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;YACrD,IAAI,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;gBAAE,aAAa,GAAG,SAAS,CAAC;QAC5D,CAAC;IACH,CAAC;IACD,MAAM,kBAAkB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC;IAEtD,MAAM,SAAS,GAAG,CAAC,CAAC,WAAW,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;IACzD,MAAM,gBAAgB,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;IAC5D,MAAM,gBAAgB,GAAG,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IAElD,MAAM,UAAU,GAAG,kBAAkB,CAAC;QACpC,gBAAgB,EAAE,cAAc;QAChC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,KAAK;QACjC,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,IAAI,MAAM;QAChD,aAAa,EAAE,IAAI;QACnB,aAAa,EAAE,IAAI;QACnB,aAAa,EAAE,kBAAkB;KAClC,CAAC,CAAC;IACH,MAAM,qBAAqB,GAAG,gBAAgB,GAAG,CAAC,UAAU,CAAC,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IACrF,MAAM,kBAAkB,GAAG,gBAAgB,GAAG,UAAU,CAAC,IAAI,CAAC;IAC9D,MAAM,kBAAkB,GAAG,qBAAqB,GAAG,gBAAgB,CAAC;IACpE,MAAM,cAAc,GAAG,CAAC,CAAC,WAAW,CAAC,QAAQ,EAAE,oBAAoB,CAAC,CAAC;IACrE,MAAM,kBAAkB,GAAG,cAAc,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC;IACnE,MAAM,qBAAqB,GAAG,cAAc,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,kBAAkB,GAAG,kBAAkB,CAAC;IAC5G,MAAM,WAAW,GAAG,qBAAqB,GAAG,kBAAkB,CAAC;IAC/D,MAAM,CAAC,IAAI,CAAC;QACV,IAAI,EAAE,QAAQ;QACd,QAAQ,EAAE,WAAW;QACrB,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,WAAW,cAAc,WAAW,aAAa,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,aAAa,YAAY,CAAC,CAAC,CAAC,KAAK,cAAc,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC,kBAAkB,SAAS,CAAC,CAAC,CAAC,GAAG,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,iBAAiB,cAAc,CAAC,CAAC,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE;KAClV,CAAC,CAAC;IAEH,WAAW;IACX,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC;IACnC,MAAM,EAAE,GAAG,aAAa,CAAC,KAAK,CAAC,EAAE,GAAG,KAAK,CAAC;IAC1C,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC;IACvC,MAAM,cAAc,GAAG,WAAW,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC;IACrD,MAAM,YAAY,GAAG,CAAC,CAAC,WAAW,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;IAC9D,MAAM,OAAO,GACX,QAAQ,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,YAAY,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC;IACpF,MAAM,eAAe,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,cAAc,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC;IACnF,MAAM,UAAU,GAAG,KAAK,CAAC,eAAe,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,WAAW,CAAC,SAAS,IAAI,IAAI,CAAC;IAC7C,MAAM,mBAAmB,GAAG,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC;IAExF,MAAM,SAAS,GAAG,kBAAkB,CAAC;QACnC,gBAAgB,EAAE,mBAAmB;QACrC,QAAQ,EAAE,CAAC;QACX,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,IAAI,MAAM;QAC/C,aAAa,EAAE,IAAI;QACnB,aAAa,EAAE,KAAK;QACpB,aAAa,EAAE,CAAC;KACjB,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,mBAAmB,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC;IAC7D,MAAM,OAAO,GAAG,qBAAqB,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC;IACrD,MAAM,CAAC,IAAI,CAAC;QACV,IAAI,EAAE,SAAS;QACf,QAAQ,EAAE,OAAO;QACjB,MAAM,EAAE,KAAK,WAAW,CAAC,EAAE,QAAQ,MAAM,CAAC,EAAE,CAAC,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,aAAa,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,EAAE,gBAAgB,mBAAmB,cAAc,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG;KAClQ,CAAC,CAAC;IAEH,YAAY;IACZ,MAAM,KAAK,GAAG,aAAa,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACnD,MAAM,KAAK,GAAG,WAAW,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC7C,MAAM,UAAU,GAAG,KAAK,IAAI,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACnF,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,UAAU,GAAG,QAAQ,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAChF,MAAM,UAAU,GAAG,OAAO,GAAG,YAAY,CAAC;IAC1C,MAAM,YAAY,GAAG,kBAAkB,GAAG,YAAY,CAAC;IACvD,MAAM,MAAM,GAAG,UAAU,GAAG,YAAY,CAAC;IACzC,MAAM,CAAC,IAAI,CAAC;QACV,IAAI,EAAE,QAAQ;QACd,QAAQ,EAAE,MAAM;QAChB,MAAM,EAAE,KAAK,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,YAAY,UAAU,eAAe,CAAC,CAAC,CAAC,EAAE,GAAG,QAAQ,CAAC,SAAS,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,YAAY,kBAAkB,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE;KAChQ,CAAC,CAAC;IAEH,SAAS;IACT,IAAI,QAAQ,GAAG,MAAM,CAAC;IACtB,IAAI,SAAS,GAAG,QAAQ,CAAC;IACzB,MAAM,GAAG,GAAG,QAAQ,CAAC,UAAU,CAAC;IAChC,IAAI,GAAG,EAAE,CAAC;QACR,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAChE,QAAQ,GAAG,MAAM,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC;QAChC,SAAS,GAAG,OAAO,GAAG,CAAC,SAAS,QAAQ,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IAC9D,CAAC;IACD,uEAAuE;IACvE,uEAAuE;IACvE,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IAE1E,mBAAmB;IACnB,MAAM,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC;IACxB,MAAM,oBAAoB,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,gBAAgB,EAAE,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAClF,MAAM,CAAC,IAAI,CAAC;QACV,IAAI,EAAE,eAAe;QACrB,QAAQ,EAAE,oBAAoB;QAC9B,MAAM,EAAE,IAAI,CAAC,eAAe,gBAAgB,sBAAsB,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,gBAAgB,GAAG;KACrK,CAAC,CAAC;IAEH,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AAC9B,CAAC;AAED,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E,qEAAqE;AACrE,SAAS,iBAAiB,CAAC,IAAU;IACnC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,IAAI,EAAE;QAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IACvE,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,gBAAgB,IAAI,EAAE;QAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IAC/E,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,eAAe,CACtB,QAA0B,EAC1B,OAAgB,EAChB,GAAkB;IAElB,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC3D,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,yEAAyE;QACzE,iDAAiD;QACjD,OAAO,wBAAwB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO,UAAU,CAAC,YAAY,CAAC,QAAQ,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;AAC7D,CAAC;AAED,SAAS,wBAAwB,CAC/B,QAA0B,EAC1B,OAAgB,EAChB,GAAkB;IAElB,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IAChE,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,MAAM,GAAG,GAAW,EAAE,CAAC;IACvB,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;QACzC,MAAM,IAAI,GAAG,OAAO,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACxD,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,GAAG,CAAC,IAAI,CACN,GAAG,IAAI,CAAC,QAAQ,CACd,GAAG,CAAC,UAAiD,EACrD,QAAQ,CAAC,MAAM,CAAC,EAAE,EAClB,GAAG,CACJ,CACF,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,WAAW,CAClB,QAA2B,EAC3B,SAAiB;IAEjB,OAAO,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,UAAU,KAAK,SAAS,CAAC,EAAE,UAAU,CAAC;AAC/F,CAAC;AAED,qEAAqE;AACrE,SAAS,cAAc,CAAC,CAAS,EAAE,CAAS;IAC1C,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC;IACzB,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC;IACpB,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACtB,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC;IACxB,OAAO,CAAC,CAAC;AACX,CAAC;AAED,6EAA6E;AAC7E,SAAS,kBAAkB,CAAC,IAQ3B;IACC,SAAS,OAAO,CAAC,IAAY;QAC3B,IAAI,IAAI,CAAC,aAAa,IAAI,IAAI,KAAK,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAClE,IAAI,IAAI,IAAI,IAAI,CAAC,aAAa;YAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAC5D,IAAI,IAAI,CAAC,aAAa,IAAI,IAAI,KAAK,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAClE,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,gBAAgB;YACpD,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE;YACtB,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAC3B,CAAC;IAED,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC;QACrC,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACvB,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC;YACd,IAAI,IAAI,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC;YACzB,SAAS;QACX,CAAC;QACD,wCAAwC;QACxC,MAAM,QAAQ,GACZ,IAAI,CAAC,MAAM,KAAK,cAAc,IAAI,CAAC,IAAI,CAAC,MAAM,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC;QAC3E,IAAI,CAAC,QAAQ;YAAE,SAAS;QACxB,6BAA6B;QAC7B,IAAI,UAAU,GAAG,CAAC,CAAC;QACnB,IAAI,UAAU,GAAG,CAAC,CAAC;QACnB,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC;YAC/B,MAAM,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC;YAC3B,UAAU,IAAI,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC;YAC9B,UAAU,IAAI,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC;QAChC,CAAC;QACD,IAAI,IAAI,UAAU,GAAG,CAAC,CAAC;QACvB,IAAI,IAAI,UAAU,GAAG,CAAC,CAAC;IACzB,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AACxB,CAAC;AAED;;;;GAIG;AACH,SAAS,aAAa,CAAC,CAAU;IAC/B,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,CAAC,CAAC;IACpC,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACjD,MAAM,OAAO,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACzB,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,CAAC,CAAC;IAC7B,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IACjC,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC/C,MAAM,KAAK,GAAG,0BAA0B,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACvD,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,GAAG,CAAC,CAAC;IAClE,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACrD,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7B,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/C,OAAO,KAAK,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC;AACxC,CAAC;AAED,SAAS,KAAK,CAAC,CAAS,EAAE,EAAU,EAAE,EAAU;IAC9C,OAAO,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;AACvC,CAAC;AAED,SAAS,MAAM,CAAC,CAAS;IACvB,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC,EAAE,CAAC;IAC1B,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC,EAAE,CAAC;IACzB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,aAAa,CACpB,MAAc,EACd,GAAW,EACX,SAAiB,EACjB,KAAa;IAEb,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,MAAM,GAAG,EAAE,CAAC,CAAC;IACrC,IAAI,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,gBAAgB,SAAS,eAAe,CAAC,CAAC;IACpE,IAAI,KAAK;QAAE,KAAK,CAAC,IAAI,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC;IAChD,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC;AAED,IAAI,gBAAgB,GAAmB,IAAI,CAAC;AAC5C,SAAS,mBAAmB;IAC1B,IAAI,CAAC,gBAAgB;QAAE,gBAAgB,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAC7D,OAAO,gBAAgB,CAAC;AAC1B,CAAC","sourcesContent":["/**\n * The expected-value damage engine.\n *\n * Closed-form math over schema profiles + a flat {@link Buff} stack. No\n * sampling, no I/O. Auto-injects every weapon-keyword on the attacker's\n * profile as a buff (so callers don't have to enumerate intrinsics), then\n * resolves the stack via {@link resolveBuffs}, then walks\n * attacks → hits → wounds → unsaved → damage → after-fnp → models-killed.\n *\n * The dataset is required (and defaults to the embedded one) — without it\n * the engine can't look up weapon-keyword effects.\n */\nimport type { Phase, Unit, Weapon } from \"../generated.js\";\nimport {\n type Buff,\n type EngineContext,\n resolveBuffs,\n type ResolvedModifiers,\n type WeaponKeywordRef,\n} from \"./buffs.js\";\nimport { Dataset } from \"../data/dataset.js\";\n\nexport type AttackProfileRef = { weapon: Weapon; profileIndex: number };\nexport type TargetProfileRef = {\n unit: Unit;\n profileIndex: number;\n /** Override target model count (otherwise read from `unit.model_count.min`). */\n modelCount?: number;\n};\n\nexport type Stage = {\n name: \"attacks\" | \"hits\" | \"wounds\" | \"unsaved\" | \"damage\" | \"after-fnp\" | \"models-killed\";\n expected: number;\n detail: string;\n};\n\nexport type EngineInput = {\n attacker: AttackProfileRef;\n target: TargetProfileRef;\n modelsFiring: number;\n /** User / ability / manual buffs. Weapon-keyword buffs are auto-injected. */\n buffs: Buff[];\n context: EngineContext;\n};\n\nexport type EngineOutput = { stages: Stage[]; resolved: ResolvedModifiers };\n\n/**\n * Compute the expected per-stage projection for one (attacker, target, buffs)\n * triple. The dataset defaults to the embedded one — pass an alternate when\n * crunching against a different bundle (e.g. tests).\n */\nexport function crunch(input: EngineInput, dataset?: Dataset): EngineOutput {\n const ds = dataset ?? lazyEmbeddedDataset();\n const weaponProfile = input.attacker.weapon.profiles[input.attacker.profileIndex];\n if (!weaponProfile) {\n throw new RangeError(\n `crunch: attacker.profileIndex=${input.attacker.profileIndex} is out of range for weapon ${input.attacker.weapon.id}`,\n );\n }\n const unitProfile = input.target.unit.profiles[input.target.profileIndex];\n if (!unitProfile) {\n throw new RangeError(\n `crunch: target.profileIndex=${input.target.profileIndex} is out of range for unit ${input.target.unit.id}`,\n );\n }\n\n const targetKeywords = unitKeywordsLower(input.target.unit);\n const ctx: EngineContext = {\n ...input.context,\n targetKeywords: input.context.targetKeywords ?? targetKeywords,\n };\n\n // Auto-inject weapon-keyword buffs from the attacker profile, then append\n // the caller-supplied stack. resolveBuffs deduplicates and ranks them.\n const profileBuffs = profileBuffsFor(input.attacker, ds, ctx);\n const resolved = resolveBuffs([...profileBuffs, ...input.buffs], ctx);\n\n const stages: Stage[] = [];\n\n // 1. Attacks\n const isMelee = input.attacker.weapon.type === \"melee\";\n const baseA = evalStatValue(weaponProfile.stats.A);\n const attacksPerModel = baseA + resolved.attacksMod.value;\n const rapidFire = findKeyword(resolved, \"rapid-fire\");\n const halfRange = ctx.withinHalfRange === true;\n const rapidFireExtraPerModel = rapidFire && halfRange ? evalStatValue(rapidFire.parameters?.value) : 0;\n const blast = findKeyword(resolved, \"blast\");\n const targetModelCount = input.target.modelCount ?? input.target.unit.model_count?.min ?? 1;\n const blastExtraPerModel = blast ? Math.floor(targetModelCount / 5) : 0;\n const attacks = input.modelsFiring * (attacksPerModel + rapidFireExtraPerModel + blastExtraPerModel);\n stages.push({\n name: \"attacks\",\n expected: attacks,\n detail: attacksDetail(input.modelsFiring, attacksPerModel, rapidFireExtraPerModel, blastExtraPerModel),\n });\n\n // 2. Hits\n const hitStat = isMelee ? weaponProfile.stats.WS : weaponProfile.stats.BS;\n const torrent = !!findKeyword(resolved, \"torrent\");\n let hits: number;\n let critHits: number;\n let hitsDetail: string;\n if (torrent) {\n hits = attacks;\n critHits = 0;\n hitsDetail = `Torrent: auto-hits (${attacks.toFixed(4)})`;\n } else {\n if (typeof hitStat !== \"number\") {\n throw new Error(\n `crunch: weapon ${input.attacker.weapon.id} profile ${input.attacker.profileIndex} missing ${isMelee ? \"WS\" : \"BS\"}`,\n );\n }\n const probs = checkProbabilities({\n unmodifiedNeeded: hitStat,\n modifier: resolved.hitMod.value,\n reroll: resolved.rerolls.hit?.subset ?? \"none\",\n autoFailOnOne: true,\n autoPassOnSix: true,\n critThreshold: 6,\n });\n hits = attacks * probs.pass;\n critHits = attacks * probs.crit;\n hitsDetail = `${isMelee ? \"WS\" : \"BS\"}${hitStat}+ (mod ${signed(resolved.hitMod.value)}, reroll ${resolved.rerolls.hit?.subset ?? \"none\"}) → P(hit)=${probs.pass.toFixed(4)}, P(crit)=${probs.crit.toFixed(4)}`;\n }\n const sustained = findKeyword(resolved, \"sustained-hits\");\n if (sustained) {\n hits += critHits * evalStatValue(sustained.parameters?.value);\n hitsDetail += `; +Sustained Hits ${sustained.parameters?.value ?? 1} on ${critHits.toFixed(4)} crits`;\n }\n stages.push({ name: \"hits\", expected: hits, detail: hitsDetail });\n\n // 3. Wounds\n const S = evalStatValue(weaponProfile.stats.S) + resolved.strengthMod.value;\n const T = unitProfile.T + resolved.toughnessMod.value;\n const stdWoundNeeded = woundThreshold(S, T);\n const anti = findKeyword(resolved, \"anti\");\n let antiThreshold = 7; // unreachable\n if (anti) {\n const targetKw = (anti.parameters?.target_keyword as string | undefined)?.toLowerCase();\n if (targetKw && targetKeywords.includes(targetKw)) {\n const threshold = Number(anti.parameters?.threshold);\n if (Number.isFinite(threshold)) antiThreshold = threshold;\n }\n }\n const critWoundThreshold = Math.min(6, antiThreshold);\n\n const hasLethal = !!findKeyword(resolved, \"lethal-hits\");\n const hitsForWoundRoll = hasLethal ? hits - critHits : hits;\n const lethalAutoWounds = hasLethal ? critHits : 0;\n\n const woundProbs = checkProbabilities({\n unmodifiedNeeded: stdWoundNeeded,\n modifier: resolved.woundMod.value,\n reroll: resolved.rerolls.wound?.subset ?? \"none\",\n autoFailOnOne: true,\n autoPassOnSix: true,\n critThreshold: critWoundThreshold,\n });\n const regularWoundsFromRoll = hitsForWoundRoll * (woundProbs.pass - woundProbs.crit);\n const critWoundsFromRoll = hitsForWoundRoll * woundProbs.crit;\n const totalRegularWounds = regularWoundsFromRoll + lethalAutoWounds;\n const hasDevastating = !!findKeyword(resolved, \"devastating-wounds\");\n const mortalWoundsStream = hasDevastating ? critWoundsFromRoll : 0;\n const regularWoundsForSaves = hasDevastating ? totalRegularWounds : totalRegularWounds + critWoundsFromRoll;\n const totalWounds = regularWoundsForSaves + mortalWoundsStream;\n stages.push({\n name: \"wounds\",\n expected: totalWounds,\n detail: `S${S} vs T${T} → need ${stdWoundNeeded}+, anti ${antiThreshold <= 6 ? `${antiThreshold}+ (active)` : \"n/a\"}, P(wound)=${woundProbs.pass.toFixed(4)} (${critWoundsFromRoll.toFixed(4)} crit), lethal ${hasLethal ? \"+\" + lethalAutoWounds.toFixed(4) : \"—\"}, devastating ${hasDevastating ? mortalWoundsStream.toFixed(4) + \" MW\" : \"—\"}`,\n });\n\n // 4. Saves\n const apMod = resolved.apMod.value;\n const AP = weaponProfile.stats.AP + apMod;\n const saveMod = resolved.saveMod.value;\n const armorTargetRaw = unitProfile.Sv - AP - saveMod;\n const ignoresCover = !!findKeyword(resolved, \"ignores-cover\");\n const covered =\n resolved.cover.active && !ignoresCover && input.attacker.weapon.type === \"ranged\";\n const armorAfterCover = covered ? Math.max(3, armorTargetRaw - 1) : armorTargetRaw;\n const armorFinal = clamp(armorAfterCover, 2, 7);\n const invuln = unitProfile.invuln_sv ?? null;\n const effectiveSaveTarget = invuln !== null ? Math.min(armorFinal, invuln) : armorFinal;\n\n const saveProbs = checkProbabilities({\n unmodifiedNeeded: effectiveSaveTarget,\n modifier: 0,\n reroll: resolved.rerolls.save?.subset ?? \"none\",\n autoFailOnOne: true,\n autoPassOnSix: false,\n critThreshold: 7,\n });\n const pSaved = effectiveSaveTarget >= 7 ? 0 : saveProbs.pass;\n const unsaved = regularWoundsForSaves * (1 - pSaved);\n stages.push({\n name: \"unsaved\",\n expected: unsaved,\n detail: `Sv${unitProfile.Sv}+, AP${signed(AP)}${apMod !== 0 ? ` (apmod ${signed(apMod)})` : \"\"}${saveMod !== 0 ? `, savemod ${signed(saveMod)}` : \"\"}${covered ? \", cover (+1, cap 3+)\" : \"\"} → effective ${effectiveSaveTarget}+ (P(save)=${pSaved.toFixed(4)})`,\n });\n\n // 5. Damage\n const baseD = evalStatValue(weaponProfile.stats.D);\n const melta = findKeyword(resolved, \"melta\");\n const meltaBonus = melta && halfRange ? evalStatValue(melta.parameters?.value) : 0;\n const damagePerHit = Math.max(0, baseD + meltaBonus + resolved.damageMod.value);\n const damageMain = unsaved * damagePerHit;\n const damageMortal = mortalWoundsStream * damagePerHit;\n const damage = damageMain + damageMortal;\n stages.push({\n name: \"damage\",\n expected: damage,\n detail: `D ${baseD}${meltaBonus ? ` + Melta ${meltaBonus} (half range)` : \"\"}${resolved.damageMod.value !== 0 ? ` ${signed(resolved.damageMod.value)} (mod)` : \"\"} = ${damagePerHit} per hit; main ${damageMain.toFixed(4)}, mortal ${damageMortal.toFixed(4)}`,\n });\n\n // 6. FNP\n let afterFnp = damage;\n let fnpDetail = \"no FNP\";\n const fnp = resolved.feelNoPain;\n if (fnp) {\n const pSucc = Math.max(0, Math.min(1, (7 - fnp.threshold) / 6));\n afterFnp = damage * (1 - pSucc);\n fnpDetail = `FNP ${fnp.threshold}+ (P=${pSucc.toFixed(4)})`;\n }\n // TODO M2: per-damage-point FNP rolls (e.g. Death Guard 5+ FNP only on\n // mortals); the current model applies FNP linearly to expected damage.\n stages.push({ name: \"after-fnp\", expected: afterFnp, detail: fnpDetail });\n\n // 7. Models killed\n const W = unitProfile.W;\n const expectedModelsKilled = W > 0 ? Math.min(targetModelCount, afterFnp / W) : 0;\n stages.push({\n name: \"models-killed\",\n expected: expectedModelsKilled,\n detail: `W${W} per model, ${targetModelCount} models in target; ${afterFnp.toFixed(4)} damage / ${W} = ${(afterFnp / W).toFixed(4)} (capped at ${targetModelCount})`,\n });\n\n return { stages, resolved };\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Lower-cased union of a unit's `keywords` + `faction_keywords`. */\nfunction unitKeywordsLower(unit: Unit): string[] {\n const out: string[] = [];\n for (const k of unit.keywords ?? []) out.push(String(k).toLowerCase());\n for (const k of unit.faction_keywords ?? []) out.push(String(k).toLowerCase());\n return out;\n}\n\nfunction profileBuffsFor(\n attacker: AttackProfileRef,\n dataset: Dataset,\n ctx: EngineContext,\n): Buff[] {\n const weaponView = dataset.weapons.get(attacker.weapon.id);\n if (!weaponView) {\n // Weapon isn't in the dataset (probably a hand-built test fixture); fall\n // back to walking its catalog keywords manually.\n return manualWeaponKeywordBuffs(attacker, dataset, ctx);\n }\n return weaponView.profileBuffs(attacker.profileIndex, ctx);\n}\n\nfunction manualWeaponKeywordBuffs(\n attacker: AttackProfileRef,\n dataset: Dataset,\n ctx: EngineContext,\n): Buff[] {\n const profile = attacker.weapon.profiles[attacker.profileIndex];\n if (!profile) return [];\n const out: Buff[] = [];\n for (const ref of profile.keywords ?? []) {\n const view = dataset.weaponKeywords.get(ref.keyword_id);\n if (!view) continue;\n out.push(\n ...view.getBuffs(\n ref.parameters as Record<string, unknown> | undefined,\n attacker.weapon.id,\n ctx,\n ),\n );\n }\n return out;\n}\n\nfunction findKeyword(\n resolved: ResolvedModifiers,\n keywordId: string,\n): WeaponKeywordRef | undefined {\n return resolved.extraKeywords.find((e) => e.keywordRef.keyword_id === keywordId)?.keywordRef;\n}\n\n/** Standard 10e S-vs-T table → unmodified wound threshold (2..6). */\nfunction woundThreshold(S: number, T: number): number {\n if (S >= 2 * T) return 2;\n if (S > T) return 3;\n if (S === T) return 4;\n if (S * 2 > T) return 5;\n return 6;\n}\n\n/** Probability a single die check passes (and the conditional crit rate). */\nfunction checkProbabilities(args: {\n unmodifiedNeeded: number;\n modifier: number;\n reroll: \"none\" | \"ones\" | \"all-failures\";\n autoFailOnOne: boolean;\n autoPassOnSix: boolean;\n /** Natural roll ≥ this is a crit. Use 7 to disable crits. */\n critThreshold: number;\n}): { pass: number; crit: number } {\n function outcome(face: number): { pass: number; crit: number } {\n if (args.autoFailOnOne && face === 1) return { pass: 0, crit: 0 };\n if (face >= args.critThreshold) return { pass: 1, crit: 1 };\n if (args.autoPassOnSix && face === 6) return { pass: 1, crit: 0 };\n return (face + args.modifier) >= args.unmodifiedNeeded\n ? { pass: 1, crit: 0 }\n : { pass: 0, crit: 0 };\n }\n\n let pass = 0;\n let crit = 0;\n for (let face = 1; face <= 6; face++) {\n const initial = outcome(face);\n if (initial.pass === 1) {\n pass += 1 / 6;\n crit += initial.crit / 6;\n continue;\n }\n // Failed initial — eligible for reroll?\n const eligible =\n args.reroll === \"all-failures\" || (args.reroll === \"ones\" && face === 1);\n if (!eligible) continue;\n // Reroll: uniform over 1..6.\n let rerollPass = 0;\n let rerollCrit = 0;\n for (let f2 = 1; f2 <= 6; f2++) {\n const second = outcome(f2);\n rerollPass += second.pass / 6;\n rerollCrit += second.crit / 6;\n }\n pass += rerollPass / 6;\n crit += rerollCrit / 6;\n }\n return { pass, crit };\n}\n\n/**\n * Mean value of a stat (number or dice expression like `\"D6\"`, `\"2D6\"`,\n * `\"D3+1\"`, `\"D6-1\"`). Unrecognised strings throw — better to crash than to\n * silently return 0 and produce a confidently wrong damage projection.\n */\nfunction evalStatValue(v: unknown): number {\n if (typeof v === \"number\") return v;\n if (typeof v !== \"string\") return Number(v) || 0;\n const trimmed = v.trim();\n if (trimmed === \"\") return 0;\n const asNumber = Number(trimmed);\n if (Number.isFinite(asNumber)) return asNumber;\n const match = /^(\\d*)D(\\d+)([+-]\\d+)?$/i.exec(trimmed);\n if (!match) throw new Error(`evalStatValue: cannot parse \"${v}\"`);\n const count = match[1] === \"\" ? 1 : Number(match[1]);\n const die = Number(match[2]);\n const offset = match[3] ? Number(match[3]) : 0;\n return count * (die + 1) / 2 + offset;\n}\n\nfunction clamp(n: number, lo: number, hi: number): number {\n return Math.max(lo, Math.min(hi, n));\n}\n\nfunction signed(n: number): string {\n if (n > 0) return `+${n}`;\n if (n < 0) return `${n}`;\n return \"0\";\n}\n\nfunction attacksDetail(\n models: number,\n per: number,\n rapidFire: number,\n blast: number,\n): string {\n const parts = [`${models} × ${per}`];\n if (rapidFire) parts.push(`+ Rapid Fire ${rapidFire} (half range)`);\n if (blast) parts.push(`+ Blast ${blast}/model`);\n return parts.join(\" \");\n}\n\nlet _embeddedDataset: Dataset | null = null;\nfunction lazyEmbeddedDataset(): Dataset {\n if (!_embeddedDataset) _embeddedDataset = Dataset.embedded();\n return _embeddedDataset;\n}\n\nexport type { Phase };\n"]}
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/cruncher/engine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The expected-value damage engine.\n *\n * Closed-form math over schema profiles + a flat {@link Buff} stack. No\n * sampling, no I/O. Auto-injects every weapon-keyword on the attacker's\n * profile as a buff (so callers don't have to enumerate intrinsics), then\n * resolves the stack via {@link resolveBuffs}, then walks\n * attacks → hits → wounds → unsaved → damage → after-fnp → models-killed.\n *\n * The dataset is required (and defaults to the embedded one) — without it\n * the engine can't look up weapon-keyword effects.\n */\nimport type { Phase, Unit, Weapon } from \"../generated.js\";\nimport {\n type Buff,\n type EngineContext,\n resolveBuffs,\n type ResolvedModifiers,\n type WeaponKeywordRef,\n} from \"./buffs.js\";\nimport { Dataset } from \"../data/dataset.js\";\n\nexport type AttackProfileRef = { weapon: Weapon; profileIndex: number };\nexport type TargetProfileRef = {\n unit: Unit;\n profileIndex: number;\n /** Override target model count (otherwise read from `unit.model_count.min`). */\n modelCount?: number;\n};\n\nexport type Stage = {\n name: \"attacks\" | \"hits\" | \"wounds\" | \"unsaved\" | \"damage\" | \"after-fnp\" | \"models-killed\";\n expected: number;\n detail: string;\n};\n\nexport type EngineInput = {\n attacker: AttackProfileRef;\n target: TargetProfileRef;\n modelsFiring: number;\n /** User / ability / manual buffs. Weapon-keyword buffs are auto-injected. */\n buffs: Buff[];\n context: EngineContext;\n};\n\nexport type EngineOutput = { stages: Stage[]; resolved: ResolvedModifiers };\n\n/**\n * Compute the expected per-stage projection for one (attacker, target, buffs)\n * triple. The dataset defaults to the embedded one — pass an alternate when\n * crunching against a different bundle (e.g. tests).\n */\nexport function crunch(input: EngineInput, dataset?: Dataset): EngineOutput {\n const ds = dataset ?? lazyEmbeddedDataset();\n const weaponProfile = input.attacker.weapon.profiles[input.attacker.profileIndex];\n if (!weaponProfile) {\n throw new RangeError(\n `crunch: attacker.profileIndex=${input.attacker.profileIndex} is out of range for weapon ${input.attacker.weapon.id}`,\n );\n }\n const unitProfile = input.target.unit.profiles[input.target.profileIndex];\n if (!unitProfile) {\n throw new RangeError(\n `crunch: target.profileIndex=${input.target.profileIndex} is out of range for unit ${input.target.unit.id}`,\n );\n }\n\n const targetKeywords = unitKeywordsLower(input.target.unit);\n const ctx: EngineContext = {\n ...input.context,\n targetKeywords: input.context.targetKeywords ?? targetKeywords,\n };\n\n // Auto-inject weapon-keyword buffs from the attacker profile, then append\n // the caller-supplied stack. resolveBuffs deduplicates and ranks them.\n const profileBuffs = profileBuffsFor(input.attacker, ds, ctx);\n const resolved = resolveBuffs([...profileBuffs, ...input.buffs], ctx);\n\n const stages: Stage[] = [];\n\n // 1. Attacks\n const isMelee = input.attacker.weapon.type === \"melee\";\n const baseA = evalStatValue(weaponProfile.stats.A);\n const attacksPerModel = baseA + resolved.attacksMod.value;\n const rapidFire = findKeyword(resolved, \"rapid-fire\");\n const halfRange = ctx.withinHalfRange === true;\n const rapidFireExtraPerModel = rapidFire && halfRange ? evalStatValue(rapidFire.parameters?.value) : 0;\n const blast = findKeyword(resolved, \"blast\");\n const targetModelCount = input.target.modelCount ?? input.target.unit.model_count?.min ?? 1;\n const blastExtraPerModel = blast ? Math.floor(targetModelCount / 5) : 0;\n const attacks = input.modelsFiring * (attacksPerModel + rapidFireExtraPerModel + blastExtraPerModel);\n stages.push({\n name: \"attacks\",\n expected: attacks,\n detail: attacksDetail(input.modelsFiring, attacksPerModel, rapidFireExtraPerModel, blastExtraPerModel),\n });\n\n // 2. Hits\n const hitStat = isMelee ? weaponProfile.stats.WS : weaponProfile.stats.BS;\n const torrent = !!findKeyword(resolved, \"torrent\");\n let hits: number;\n let critHits: number;\n let hitsDetail: string;\n if (torrent) {\n hits = attacks;\n critHits = 0;\n hitsDetail = `Torrent: auto-hits (${attacks.toFixed(4)})`;\n } else {\n if (typeof hitStat !== \"number\") {\n throw new Error(\n `crunch: weapon ${input.attacker.weapon.id} profile ${input.attacker.profileIndex} missing ${isMelee ? \"WS\" : \"BS\"}`,\n );\n }\n const probs = checkProbabilities({\n unmodifiedNeeded: hitStat,\n modifier: resolved.hitMod.value,\n reroll: resolved.rerolls.hit?.subset ?? \"none\",\n autoFailOnOne: true,\n autoPassOnSix: true,\n critThreshold: 6,\n });\n hits = attacks * probs.pass;\n critHits = attacks * probs.crit;\n hitsDetail = `${isMelee ? \"WS\" : \"BS\"}${hitStat}+ (mod ${signed(resolved.hitMod.value)}, reroll ${resolved.rerolls.hit?.subset ?? \"none\"}) → P(hit)=${probs.pass.toFixed(4)}, P(crit)=${probs.crit.toFixed(4)}`;\n }\n const sustained = findKeyword(resolved, \"sustained-hits\");\n if (sustained) {\n hits += critHits * evalStatValue(sustained.parameters?.value);\n hitsDetail += `; +Sustained Hits ${sustained.parameters?.value ?? 1} on ${critHits.toFixed(4)} crits`;\n }\n stages.push({ name: \"hits\", expected: hits, detail: hitsDetail });\n\n // 3. Wounds\n const S = evalStatValue(weaponProfile.stats.S) + resolved.strengthMod.value;\n const T = unitProfile.T + resolved.toughnessMod.value;\n const stdWoundNeeded = woundThreshold(S, T);\n const anti = findKeyword(resolved, \"anti\");\n let antiThreshold = 7; // unreachable\n if (anti) {\n const targetKw = (anti.parameters?.target_keyword as string | undefined)?.toLowerCase();\n if (targetKw && targetKeywords.includes(targetKw)) {\n const threshold = Number(anti.parameters?.threshold);\n if (Number.isFinite(threshold)) antiThreshold = threshold;\n }\n }\n const critWoundThreshold = Math.min(6, antiThreshold);\n\n const hasLethal = !!findKeyword(resolved, \"lethal-hits\");\n const hitsForWoundRoll = hasLethal ? hits - critHits : hits;\n const lethalAutoWounds = hasLethal ? critHits : 0;\n\n const woundProbs = checkProbabilities({\n unmodifiedNeeded: stdWoundNeeded,\n modifier: resolved.woundMod.value,\n reroll: resolved.rerolls.wound?.subset ?? \"none\",\n autoFailOnOne: true,\n autoPassOnSix: true,\n critThreshold: critWoundThreshold,\n });\n const regularWoundsFromRoll = hitsForWoundRoll * (woundProbs.pass - woundProbs.crit);\n const critWoundsFromRoll = hitsForWoundRoll * woundProbs.crit;\n const totalRegularWounds = regularWoundsFromRoll + lethalAutoWounds;\n const hasDevastating = !!findKeyword(resolved, \"devastating-wounds\");\n const mortalWoundsStream = hasDevastating ? critWoundsFromRoll : 0;\n const regularWoundsForSaves = hasDevastating ? totalRegularWounds : totalRegularWounds + critWoundsFromRoll;\n const totalWounds = regularWoundsForSaves + mortalWoundsStream;\n stages.push({\n name: \"wounds\",\n expected: totalWounds,\n detail: `S${S} vs T${T} → need ${stdWoundNeeded}+, anti ${antiThreshold <= 6 ? `${antiThreshold}+ (active)` : \"n/a\"}, P(wound)=${woundProbs.pass.toFixed(4)} (${critWoundsFromRoll.toFixed(4)} crit), lethal ${hasLethal ? \"+\" + lethalAutoWounds.toFixed(4) : \"—\"}, devastating ${hasDevastating ? mortalWoundsStream.toFixed(4) + \" MW\" : \"—\"}`,\n });\n\n // 4. Saves\n const apMod = resolved.apMod.value;\n const AP = weaponProfile.stats.AP + apMod;\n const saveMod = resolved.saveMod.value;\n const armorTargetRaw = unitProfile.Sv - AP - saveMod;\n const ignoresCover = !!findKeyword(resolved, \"ignores-cover\");\n const covered =\n resolved.cover.active && !ignoresCover && input.attacker.weapon.type === \"ranged\";\n const armorAfterCover = covered ? Math.max(3, armorTargetRaw - 1) : armorTargetRaw;\n const armorFinal = clamp(armorAfterCover, 2, 7);\n // The unit's printed invuln (from the profile) and any ability-granted\n // invuln combine best-wins (lowest threshold). Invuln bypasses AP and cover\n // — only the armor branch above is affected by those — so the final save is\n // min(armor-after-AP-and-cover, effective-invuln).\n const printedInvuln = unitProfile.invuln_sv ?? null;\n const abilityInvuln = resolved.invulnerable?.threshold ?? null;\n const effectiveInvuln =\n printedInvuln !== null && abilityInvuln !== null\n ? Math.min(printedInvuln, abilityInvuln)\n : (printedInvuln ?? abilityInvuln);\n const effectiveSaveTarget =\n effectiveInvuln !== null ? Math.min(armorFinal, effectiveInvuln) : armorFinal;\n\n const saveProbs = checkProbabilities({\n unmodifiedNeeded: effectiveSaveTarget,\n modifier: 0,\n reroll: resolved.rerolls.save?.subset ?? \"none\",\n autoFailOnOne: true,\n autoPassOnSix: false,\n critThreshold: 7,\n });\n const pSaved = effectiveSaveTarget >= 7 ? 0 : saveProbs.pass;\n const unsaved = regularWoundsForSaves * (1 - pSaved);\n stages.push({\n name: \"unsaved\",\n expected: unsaved,\n detail: `Sv${unitProfile.Sv}+, AP${signed(AP)}${apMod !== 0 ? ` (apmod ${signed(apMod)})` : \"\"}${saveMod !== 0 ? `, savemod ${signed(saveMod)}` : \"\"}${covered ? \", cover (+1, cap 3+)\" : \"\"}${abilityInvuln !== null ? `, invuln ${abilityInvuln}+ (ability)` : \"\"} → effective ${effectiveSaveTarget}+ (P(save)=${pSaved.toFixed(4)})`,\n });\n\n // 5. Damage\n const baseD = evalStatValue(weaponProfile.stats.D);\n const melta = findKeyword(resolved, \"melta\");\n const meltaBonus = melta && halfRange ? evalStatValue(melta.parameters?.value) : 0;\n const beforeReduction = Math.max(0, baseD + meltaBonus + resolved.damageMod.value);\n const damageReduction = resolved.damageReduction.value;\n // 10e damage-reduction abilities always carry the canonical \"to a minimum\n // of 1\" clause, so the floor lives in the math, not the data. The clause\n // only applies when damage-reduction is active — without it, a D1 weapon\n // with a -1 attacker damage-mod still produces 0 damage.\n const damagePerHit =\n damageReduction > 0 ? Math.max(1, beforeReduction - damageReduction) : beforeReduction;\n const damageMain = unsaved * damagePerHit;\n const damageMortal = mortalWoundsStream * damagePerHit;\n const damage = damageMain + damageMortal;\n stages.push({\n name: \"damage\",\n expected: damage,\n detail: `D ${baseD}${meltaBonus ? ` + Melta ${meltaBonus} (half range)` : \"\"}${resolved.damageMod.value !== 0 ? ` ${signed(resolved.damageMod.value)} (mod)` : \"\"}${damageReduction > 0 ? ` -${damageReduction} (defender, min 1)` : \"\"} = ${damagePerHit} per hit; main ${damageMain.toFixed(4)}, mortal ${damageMortal.toFixed(4)}`,\n });\n\n // 6. FNP\n // Two scopes compose: an all-FNP fires on every unsaved wound; a mortal-FNP\n // fires only on the mortal-wound stream (e.g. Death Guard 5+ FNP vs mortals).\n // A target carrying both rolls both against mortals — independent Bernoulli\n // trials, so the surviving fractions multiply.\n const pSurviveAll = fnpSurvivalFraction(resolved.feelNoPain);\n const pSurviveMortal = fnpSurvivalFraction(resolved.feelNoPainMortal);\n const afterMain = damageMain * pSurviveAll;\n const afterMortal = damageMortal * pSurviveAll * pSurviveMortal;\n const afterFnp = afterMain + afterMortal;\n const fnpDetail = describeFnp(resolved.feelNoPain, resolved.feelNoPainMortal);\n stages.push({ name: \"after-fnp\", expected: afterFnp, detail: fnpDetail });\n\n // 7. Models killed\n const W = unitProfile.W;\n const expectedModelsKilled = W > 0 ? Math.min(targetModelCount, afterFnp / W) : 0;\n stages.push({\n name: \"models-killed\",\n expected: expectedModelsKilled,\n detail: `W${W} per model, ${targetModelCount} models in target; ${afterFnp.toFixed(4)} damage / ${W} = ${(afterFnp / W).toFixed(4)} (capped at ${targetModelCount})`,\n });\n\n return { stages, resolved };\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Lower-cased union of a unit's `keywords` + `faction_keywords`. */\nfunction unitKeywordsLower(unit: Unit): string[] {\n const out: string[] = [];\n for (const k of unit.keywords ?? []) out.push(String(k).toLowerCase());\n for (const k of unit.faction_keywords ?? []) out.push(String(k).toLowerCase());\n return out;\n}\n\nfunction profileBuffsFor(\n attacker: AttackProfileRef,\n dataset: Dataset,\n ctx: EngineContext,\n): Buff[] {\n const weaponView = dataset.weapons.get(attacker.weapon.id);\n if (!weaponView) {\n // Weapon isn't in the dataset (probably a hand-built test fixture); fall\n // back to walking its catalog keywords manually.\n return manualWeaponKeywordBuffs(attacker, dataset, ctx);\n }\n return weaponView.profileBuffs(attacker.profileIndex, ctx);\n}\n\nfunction manualWeaponKeywordBuffs(\n attacker: AttackProfileRef,\n dataset: Dataset,\n ctx: EngineContext,\n): Buff[] {\n const profile = attacker.weapon.profiles[attacker.profileIndex];\n if (!profile) return [];\n const out: Buff[] = [];\n for (const ref of profile.keywords ?? []) {\n const view = dataset.weaponKeywords.get(ref.keyword_id);\n if (!view) continue;\n out.push(\n ...view.getBuffs(\n ref.parameters as Record<string, unknown> | undefined,\n attacker.weapon.id,\n ctx,\n ),\n );\n }\n return out;\n}\n\nfunction findKeyword(\n resolved: ResolvedModifiers,\n keywordId: string,\n): WeaponKeywordRef | undefined {\n return resolved.extraKeywords.find((e) => e.keywordRef.keyword_id === keywordId)?.keywordRef;\n}\n\n/** Standard 10e S-vs-T table → unmodified wound threshold (2..6). */\nfunction woundThreshold(S: number, T: number): number {\n if (S >= 2 * T) return 2;\n if (S > T) return 3;\n if (S === T) return 4;\n if (S * 2 > T) return 5;\n return 6;\n}\n\n/** Probability a single die check passes (and the conditional crit rate). */\nfunction checkProbabilities(args: {\n unmodifiedNeeded: number;\n modifier: number;\n reroll: \"none\" | \"ones\" | \"all-failures\";\n autoFailOnOne: boolean;\n autoPassOnSix: boolean;\n /** Natural roll ≥ this is a crit. Use 7 to disable crits. */\n critThreshold: number;\n}): { pass: number; crit: number } {\n function outcome(face: number): { pass: number; crit: number } {\n if (args.autoFailOnOne && face === 1) return { pass: 0, crit: 0 };\n if (face >= args.critThreshold) return { pass: 1, crit: 1 };\n if (args.autoPassOnSix && face === 6) return { pass: 1, crit: 0 };\n return (face + args.modifier) >= args.unmodifiedNeeded\n ? { pass: 1, crit: 0 }\n : { pass: 0, crit: 0 };\n }\n\n let pass = 0;\n let crit = 0;\n for (let face = 1; face <= 6; face++) {\n const initial = outcome(face);\n if (initial.pass === 1) {\n pass += 1 / 6;\n crit += initial.crit / 6;\n continue;\n }\n // Failed initial — eligible for reroll?\n const eligible =\n args.reroll === \"all-failures\" || (args.reroll === \"ones\" && face === 1);\n if (!eligible) continue;\n // Reroll: uniform over 1..6.\n let rerollPass = 0;\n let rerollCrit = 0;\n for (let f2 = 1; f2 <= 6; f2++) {\n const second = outcome(f2);\n rerollPass += second.pass / 6;\n rerollCrit += second.crit / 6;\n }\n pass += rerollPass / 6;\n crit += rerollCrit / 6;\n }\n return { pass, crit };\n}\n\n/**\n * Mean value of a stat (number or dice expression like `\"D6\"`, `\"2D6\"`,\n * `\"D3+1\"`, `\"D6-1\"`). Unrecognised strings throw — better to crash than to\n * silently return 0 and produce a confidently wrong damage projection.\n */\nfunction evalStatValue(v: unknown): number {\n if (typeof v === \"number\") return v;\n if (typeof v !== \"string\") return Number(v) || 0;\n const trimmed = v.trim();\n if (trimmed === \"\") return 0;\n const asNumber = Number(trimmed);\n if (Number.isFinite(asNumber)) return asNumber;\n const match = /^(\\d*)D(\\d+)([+-]\\d+)?$/i.exec(trimmed);\n if (!match) throw new Error(`evalStatValue: cannot parse \"${v}\"`);\n const count = match[1] === \"\" ? 1 : Number(match[1]);\n const die = Number(match[2]);\n const offset = match[3] ? Number(match[3]) : 0;\n return count * (die + 1) / 2 + offset;\n}\n\nfunction clamp(n: number, lo: number, hi: number): number {\n return Math.max(lo, Math.min(hi, n));\n}\n\nfunction signed(n: number): string {\n if (n > 0) return `+${n}`;\n if (n < 0) return `${n}`;\n return \"0\";\n}\n\n/** Fraction of damage that survives a single FNP roll (1 if no FNP). */\nfunction fnpSurvivalFraction(\n fnp: { threshold: number; dominantSource: unknown } | null,\n): number {\n if (!fnp) return 1;\n const pSucc = Math.max(0, Math.min(1, (7 - fnp.threshold) / 6));\n return 1 - pSucc;\n}\n\nfunction describeFnp(\n all: { threshold: number; dominantSource: unknown } | null,\n mortal: { threshold: number; dominantSource: unknown } | null,\n): string {\n if (!all && !mortal) return \"no FNP\";\n const parts: string[] = [];\n if (all) {\n const pSucc = (7 - all.threshold) / 6;\n parts.push(`FNP ${all.threshold}+ (P=${pSucc.toFixed(4)})`);\n }\n if (mortal) {\n const pSucc = (7 - mortal.threshold) / 6;\n parts.push(`FNP ${mortal.threshold}+ vs mortals (P=${pSucc.toFixed(4)})`);\n }\n return parts.join(\", \");\n}\n\nfunction attacksDetail(\n models: number,\n per: number,\n rapidFire: number,\n blast: number,\n): string {\n const parts = [`${models} × ${per}`];\n if (rapidFire) parts.push(`+ Rapid Fire ${rapidFire} (half range)`);\n if (blast) parts.push(`+ Blast ${blast}/model`);\n return parts.join(\" \");\n}\n\nlet _embeddedDataset: Dataset | null = null;\nfunction lazyEmbeddedDataset(): Dataset {\n if (!_embeddedDataset) _embeddedDataset = Dataset.embedded();\n return _embeddedDataset;\n}\n\nexport type { Phase };\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"from-dsl.d.ts","sourceRoot":"","sources":["../../src/cruncher/from-dsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EACV,IAAI,EAGJ,UAAU,EACV,aAAa,EACb,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAGpB,8EAA8E;AAC9E,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,kEAAkE;IAClE,EAAE,EAAE,MAAM,CAAC;IACX,0EAA0E;IAC1E,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,6DAA6D;IAC7D,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,IAAI,EAAE,CAAC;IAChB,WAAW,EAAE,mBAAmB,EAAE,CAAC;IACnC,4EAA4E;IAC5E,WAAW,EAAE,eAAe,EAAE,CAAC;CAChC,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,sBAAsB,GAAG,UAAU,GAAG,QAAQ,CAAC;AAiB3D;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,aAAa,EACtB,WAAW,GAAE,sBAAmC,GAC/C,iBAAiB,CAKnB;
|
|
1
|
+
{"version":3,"file":"from-dsl.d.ts","sourceRoot":"","sources":["../../src/cruncher/from-dsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EACV,IAAI,EAGJ,UAAU,EACV,aAAa,EACb,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAGpB,8EAA8E;AAC9E,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,kEAAkE;IAClE,EAAE,EAAE,MAAM,CAAC;IACX,0EAA0E;IAC1E,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,6DAA6D;IAC7D,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,IAAI,EAAE,CAAC;IAChB,WAAW,EAAE,mBAAmB,EAAE,CAAC;IACnC,4EAA4E;IAC5E,WAAW,EAAE,eAAe,EAAE,CAAC;CAChC,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,sBAAsB,GAAG,UAAU,GAAG,QAAQ,CAAC;AAiB3D;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,aAAa,EACtB,WAAW,GAAE,sBAAmC,GAC/C,iBAAiB,CAKnB;AA0jCD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAuBtE"}
|