diff --git a/api/constants.ts b/api/constants.ts index b5e08e11..40ee474f 100644 --- a/api/constants.ts +++ b/api/constants.ts @@ -15,4 +15,10 @@ export const getCharacteristic = (val: string): Characteristic | null => { return null; }; +export const getCharacteristicsAfter = (characteristic: Characteristic): Characteristic[] => { + const values = Object.keys(Characteristic).map((key) => Characteristic[key]); + const index = values.indexOf(characteristic); + return values.slice(index + 1); +}; + export const SAVES = [2, 3, 4, 5, 6, 0]; diff --git a/api/processors/maxDamageProcessor.ts b/api/processors/maxDamageProcessor.ts index f31d21dc..d464b1aa 100644 --- a/api/processors/maxDamageProcessor.ts +++ b/api/processors/maxDamageProcessor.ts @@ -1,4 +1,4 @@ -import { Characteristic as C } from '../constants'; +import { Characteristic as C, getCharacteristicsAfter } from '../constants'; import { MODIFIERS as m } from '../models/modifiers'; import type WeaponProfile from '../models/weaponProfile'; @@ -31,9 +31,22 @@ export default class MaxDamageProcessor { private getMaxDamageForModel(): number { const { attacks, damage } = this.profile; - const attacksMax = this.resolveExplodingModifiers(attacks.max + this.resolveBonusModifiers(C.ATTACKS)); - const damageMax = this.resolveMortalWounds(damage.max + this.resolveBonusModifiers(C.DAMAGE)); - return attacksMax * damageMax; + const baseAttacks = attacks.max + this.resolveBonusModifiers(C.ATTACKS); + + const baseDamage = this.resolveMortalWounds(damage.max + this.resolveBonusModifiers(C.DAMAGE)); + const fullConditionalDamage = baseDamage + this.getConditionalBonusDamage(null, C.DAMAGE); + + const baseAttacksDamage = baseAttacks * fullConditionalDamage; + + return m.EXPLODING.availableCharacteristics.reduce((acc, c) => { + const mod = this.profile.modifiers.getModifier(m.EXPLODING, c); + if (mod && mod.extraHits.max > 0) { + const extraHits = baseAttacks * mod.extraHits.max; + const conditionalDamage = baseDamage + this.getConditionalBonusDamage(c, C.DAMAGE); + return acc + extraHits * conditionalDamage; + } + return acc; + }, baseAttacksDamage); } private resolveBonusModifiers(characteristic: C): number { @@ -42,25 +55,26 @@ export default class MaxDamageProcessor { if (modList && modList.length) { bonus += modList.reduce((acc, mod) => acc + mod.bonus.max, 0); } + return bonus; + } - m.CONDITIONAL_BONUS.availableCharacteristics.forEach((c) => { + private getConditionalBonusDamage(fromCharacteristic: C | null, forCharacteristic: C): number { + let bonus = 0; + let { availableCharacteristics } = m.CONDITIONAL_BONUS; + if (fromCharacteristic !== null) { + availableCharacteristics = availableCharacteristics.filter((val) => + getCharacteristicsAfter(fromCharacteristic).includes(val), + ); + } + availableCharacteristics.forEach((c) => { const mod = this.profile.modifiers.getModifier(m.CONDITIONAL_BONUS, c); - if (mod && mod.bonusToCharacteristic === characteristic) { + if (mod && mod.bonusToCharacteristic === forCharacteristic) { bonus += mod.bonus.max; } }); - return bonus; } - private resolveExplodingModifiers(numAttacks: number): number { - return m.EXPLODING.availableCharacteristics.reduce((acc, c) => { - const mod = this.profile.modifiers.getModifier(m.EXPLODING, c); - if (mod && mod.extraHits.max > 0) return acc + numAttacks * mod.extraHits.max; - return acc; - }, numAttacks); - } - private resolveMortalWounds(currentMax: number): number { let cumulativeMax = currentMax; let discreteMax = 0; diff --git a/api/tests/maxDamageProcessor.test.ts b/api/tests/maxDamageProcessor.test.ts index 95fdf4f7..3de2e5eb 100644 --- a/api/tests/maxDamageProcessor.test.ts +++ b/api/tests/maxDamageProcessor.test.ts @@ -52,4 +52,9 @@ describe('MaxDamageProcessor', () => { test('Rattling Gunners', () => { expect(u.rattlingGunners.maxDamage()).toEqual(12); }); + + test('Edge Cases', () => { + expect(u.explodingAndConditionalSame.maxDamage()).toEqual(21); + expect(u.explodingAndConditionalDifferent.maxDamage()).toEqual(27); + }); }); diff --git a/api/tests/utils/units.ts b/api/tests/utils/units.ts index b5c502e4..a33a314d 100644 --- a/api/tests/utils/units.ts +++ b/api/tests/utils/units.ts @@ -130,3 +130,42 @@ export const plagueMonksOld = new Unit('Plague Monks (pre Dec 2019 FAQ)', [ export const rattlingGunners = new Unit('Rattling Gunners', [ new WeaponProfile(1, DiceValue.parse('2D6'), 4, 4, 1, 1, []), ]); + +// #region edge cases +export const explodingAndConditionalSame = new Unit('Exploding And Conditional (Same Characteristic)', [ + new WeaponProfile(1, 3, 3, 4, 1, 2, [ + new m.EXPLODING({ + characteristic: C.TO_HIT, + on: 6, + extraHits: 2, + unmodified: true, + }), + new m.CONDITIONAL_BONUS({ + characteristic: C.TO_HIT, + bonus: 1, + unmodified: true, + bonusToCharacteristic: C.DAMAGE, + }), + ]), +]); + +export const explodingAndConditionalDifferent = new Unit( + 'Exploding And Conditional (Different Characteristic)', + [ + new WeaponProfile(1, 3, 3, 4, 1, 2, [ + new m.EXPLODING({ + characteristic: C.TO_HIT, + on: 6, + extraHits: 2, + unmodified: true, + }), + new m.CONDITIONAL_BONUS({ + characteristic: C.TO_WOUND, + bonus: 1, + unmodified: true, + bonusToCharacteristic: C.DAMAGE, + }), + ]), + ], +); +// #endregion diff --git a/client/package.json b/client/package.json index 296637aa..56c7f6ba 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "statshammer", - "version": "2.1.0", + "version": "2.1.1", "private": true, "proxy": "http://localhost:5000/", "dependencies": { diff --git a/lerna.json b/lerna.json index 9c18370f..75832d1a 100644 --- a/lerna.json +++ b/lerna.json @@ -3,6 +3,6 @@ "*", "." ], - "version": "2.1.0", + "version": "2.1.1", "npmClient": "yarn" } diff --git a/package.json b/package.json index 083c3101..e526500f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "statshammer-express", "private": true, - "version": "2.1.0", + "version": "2.1.1", "engines": { "node": "12.16.3", "yarn": "1.22.4"