Use a new algorithm using the median average deviation

This commit is contained in:
Carlos Fernandez 2026-01-20 19:59:04 -05:00
parent 81bdc7901d
commit 50ee1ccd5b
3 changed files with 134 additions and 74 deletions

View file

@ -507,14 +507,11 @@ export const subclassFeatureLabels = {
*/
/**
* @type {Record<string, Record<2 | 3 | 4, TierData> & Record<1, { damage: number[] }>}
* @type {Record<string, Record<2 | 3 | 4, TierData>}
* Scaling data used to change an adversary's tier. Each rank is applied incrementally.
*/
export const adversaryScalingData = {
bruiser: {
1: {
damage: [8, 11]
},
2: {
difficulty: 2,
majorThreshold: 5,
@ -522,7 +519,6 @@ export const adversaryScalingData = {
hp: 1,
stress: 2,
attack: 2,
damage: [12, 16]
},
3: {
difficulty: 2,
@ -531,7 +527,6 @@ export const adversaryScalingData = {
hp: 1,
stress: 0,
attack: 2,
damage: [18, 22]
},
4: {
difficulty: 2,
@ -540,13 +535,9 @@ export const adversaryScalingData = {
hp: 1,
stress: 0,
attack: 2,
damage: [30, 45]
}
},
horde: {
1: {
damage: [5, 8]
},
2: {
difficulty: 2,
majorThreshold: 5,
@ -554,7 +545,6 @@ export const adversaryScalingData = {
hp: 2,
stress: 0,
attack: 0,
damage: [9, 13]
},
3: {
difficulty: 2,
@ -563,7 +553,6 @@ export const adversaryScalingData = {
hp: 0,
stress: 1,
attack: 1,
damage: [14, 19]
},
4: {
difficulty: 2,
@ -572,13 +561,9 @@ export const adversaryScalingData = {
hp: 2,
stress: 0,
attack: 0,
damage: [20, 30]
}
},
leader: {
1: {
damage: [6, 9]
},
2: {
difficulty: 2,
majorThreshold: 6,
@ -586,7 +571,6 @@ export const adversaryScalingData = {
hp: 0,
stress: 0,
attack: 1,
damage: [12, 15]
},
3: {
difficulty: 2,
@ -595,7 +579,6 @@ export const adversaryScalingData = {
hp: 1,
stress: 0,
attack: 2,
damage: [15, 18]
},
4: {
difficulty: 2,
@ -604,13 +587,9 @@ export const adversaryScalingData = {
hp: 1,
stress: 1,
attack: 3,
damage: [25, 35]
}
},
minion: {
1: {
damage: [1, 3]
},
2: {
difficulty: 2,
majorThreshold: 0,
@ -618,7 +597,6 @@ export const adversaryScalingData = {
hp: 0,
stress: 0,
attack: 1,
damage: [2, 4]
},
3: {
difficulty: 2,
@ -627,7 +605,6 @@ export const adversaryScalingData = {
hp: 0,
stress: 1,
attack: 1,
damage: [5, 8]
},
4: {
difficulty: 2,
@ -636,13 +613,9 @@ export const adversaryScalingData = {
hp: 0,
stress: 0,
attack: 1,
damage: [10, 12]
}
},
ranged: {
1: {
damage: [6, 9]
},
2: {
difficulty: 2,
majorThreshold: 3,
@ -650,7 +623,6 @@ export const adversaryScalingData = {
hp: 1,
stress: 0,
attack: 1,
damage: [12, 16]
},
3: {
difficulty: 2,
@ -659,7 +631,6 @@ export const adversaryScalingData = {
hp: 1,
stress: 1,
attack: 2,
damage: [15, 18]
},
4: {
difficulty: 2,
@ -668,13 +639,9 @@ export const adversaryScalingData = {
hp: 1,
stress: 1,
attack: 1,
damage: [25, 35]
}
},
skulk: {
1: {
damage: [5, 8]
},
2: {
difficulty: 2,
majorThreshold: 3,
@ -682,7 +649,6 @@ export const adversaryScalingData = {
hp: 1,
stress: 1,
attack: 1,
damage: [9, 13]
},
3: {
difficulty: 2,
@ -691,7 +657,6 @@ export const adversaryScalingData = {
hp: 1,
stress: 1,
attack: 1,
damage: [14, 18]
},
4: {
difficulty: 2,
@ -700,13 +665,9 @@ export const adversaryScalingData = {
hp: 1,
stress: 1,
attack: 1,
damage: [20, 35]
}
},
solo: {
1: {
damage: [8, 11]
},
2: {
difficulty: 2,
majorThreshold: 5,
@ -714,7 +675,6 @@ export const adversaryScalingData = {
hp: 0,
stress: 1,
attack: 2,
damage: [15, 20]
},
3: {
difficulty: 2,
@ -723,7 +683,6 @@ export const adversaryScalingData = {
hp: 2,
stress: 1,
attack: 2,
damage: [20, 30]
},
4: {
difficulty: 2,
@ -732,13 +691,9 @@ export const adversaryScalingData = {
hp: 0,
stress: 1,
attack: 3,
damage: [30, 45]
}
},
standard: {
1: {
damage: [4, 6]
},
2: {
difficulty: 2,
majorThreshold: 3,
@ -746,7 +701,6 @@ export const adversaryScalingData = {
hp: 0,
stress: 0,
attack: 1,
damage: [8, 12]
},
3: {
difficulty: 2,
@ -755,7 +709,6 @@ export const adversaryScalingData = {
hp: 1,
stress: 1,
attack: 1,
damage: [12, 17]
},
4: {
difficulty: 2,
@ -764,13 +717,9 @@ export const adversaryScalingData = {
hp: 0,
stress: 1,
attack: 1,
damage: [17, 20]
}
},
support: {
1: {
damage: [3, 5]
},
2: {
difficulty: 2,
majorThreshold: 3,
@ -778,7 +727,6 @@ export const adversaryScalingData = {
hp: 1,
stress: 1,
attack: 1,
damage: [5, 12]
},
3: {
difficulty: 2,
@ -787,7 +735,6 @@ export const adversaryScalingData = {
hp: 0,
stress: 0,
attack: 1,
damage: [13, 16]
},
4: {
difficulty: 2,
@ -796,7 +743,22 @@ export const adversaryScalingData = {
hp: 1,
stress: 1,
attack: 1,
damage: [18, 25]
}
}
};
/** Scaling data used for an adversary's damage */
export const adversaryExpectedDamage = {
basic: {
1: { medianDamage: 7.5, damageDeviation: 1 },
2: { medianDamage: 13, damageDeviation: 2 },
3: { medianDamage: 15.5, damageDeviation: 1.5 },
4: { medianDamage: 27, damageDeviation: 3 }
},
minion: {
1: { medianDamage: 2, damageDeviation: 1 },
2: { medianDamage: 5, damageDeviation: 0.5 },
3: { medianDamage: 6.5, damageDeviation: 1.5 },
4: { medianDamage: 11, damageDeviation: 1 }
}
};

View file

@ -3,7 +3,7 @@ import { ActionField } from '../fields/actionField.mjs';
import BaseDataActor, { commonActorRules } from './base.mjs';
import { resourceField, bonusField } from '../fields/actorField.mjs';
import { parseTermsFromSimpleFormula } from '../../helpers/utils.mjs';
import { adversaryScalingData } from '../../config/actorConfig.mjs';
import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs';
export default class DhpAdversary extends BaseDataActor {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Adversary'];
@ -214,18 +214,11 @@ export default class DhpAdversary extends BaseDataActor {
source.system.attack.roll.bonus += scale * entry.attack;
}
/** Returns the mean and standard deviation of a series of average damages */
const analyzeDamageRange = range => {
if (range.length <= 1) throw Error('Unexpected damage range, must have at least two entries');
const mean = range.reduce((a, b) => a + b, 0) / range.length;
const deviations = range.map(r => r - mean);
const standardDeviation = Math.sqrt(deviations.reduce((r, d) => r + d * d, 0) / (range.length - 1));
return { mean, standardDeviation };
};
// Calculate mean and standard deviation of expected damage ranges in each tier. Also create a function to remap damage
const currentDamageRange = analyzeDamageRange(typeData[source.system.tier].damage);
const newDamageRange = analyzeDamageRange(typeData[tier].damage);
const expectedDamageData = adversaryExpectedDamage[source.system.type] ?? adversaryExpectedDamage.basic;
const currentDamageRange = expectedDamageData[source.system.tier];
const newDamageRange = expectedDamageData[tier];
const convertDamage = (damage, newMean) => {
const hitPointParts = damage.parts.filter(d => d.applyTo === 'hitPoints');
if (hitPointParts.length === 1 && !hitPointParts[0].value.custom.enabled) {
@ -248,14 +241,14 @@ export default class DhpAdversary extends BaseDataActor {
)
.join('+');
const terms = parseTermsFromSimpleFormula(formula);
const mean = terms.reduce((r, t) => r + (t.modifier ?? 0) + (t.dice ? (t.dice * (t.faces + 1)) / 2 : 0), 0);
return { formula, terms, mean };
const expected = terms.reduce((r, t) => r + (t.modifier ?? 0) + (t.dice ? (t.dice * (t.faces + 1)) / 2 : 0), 0);
return { formula, terms, expected };
};
// Update damage of base attack
const atkAverage = parseDamage(source.system.attack.damage).mean;
const deviation = (atkAverage - currentDamageRange.mean) / currentDamageRange.standardDeviation;
const newAtkAverage = newDamageRange.mean + newDamageRange.standardDeviation * deviation;
const atkAverage = parseDamage(source.system.attack.damage).expected;
const deviation = (atkAverage - currentDamageRange.medianDamage) / currentDamageRange.damageDeviation;
const newAtkAverage = newDamageRange.medianDamage + newDamageRange.damageDeviation * deviation;
const damage = source.system.attack.damage;
convertDamage(damage, newAtkAverage);
@ -266,11 +259,11 @@ export default class DhpAdversary extends BaseDataActor {
for (const action of Object.values(item.system.actions)) {
const damage = action.damage;
if (!damage) continue;
const { formula, mean } = parseDamage(damage);
const { formula, expected: mean } = parseDamage(damage);
if (mean === 0) continue;
const deviation = (mean - currentDamageRange.mean) / currentDamageRange.standardDeviation;
const newMean = newDamageRange.mean + newDamageRange.standardDeviation * deviation;
const deviation = (mean - currentDamageRange.medianDamage) / currentDamageRange.damageDeviation;
const newMean = newDamageRange.medianDamage + newDamageRange.damageDeviation * deviation;
convertDamage(damage, newMean);
const oldFormulaRegexp = new RegExp(formula.replace('+', '(?:\\s)?\\+(?:\\s)?'));