diff --git a/lang/en.json b/lang/en.json
index 1105bef6..8e64ab7d 100755
--- a/lang/en.json
+++ b/lang/en.json
@@ -2849,9 +2849,7 @@
"tier": "Tier {tier} {type}",
"character": "Level {level} Character",
"companion": "Level {level} - {partner}",
- "companionNoPartner": "No Partner",
- "duplicateToNewTier": "Duplicate to New Tier",
- "pickTierTitle": "Pick a new tier for this adversary"
+ "companionNoPartner": "No Partner"
},
"daggerheartMenu": {
"title": "Daggerheart Menu",
diff --git a/module/applications/sidebar/tabs/actorDirectory.mjs b/module/applications/sidebar/tabs/actorDirectory.mjs
index 9d8f16e1..86a5be56 100644
--- a/module/applications/sidebar/tabs/actorDirectory.mjs
+++ b/module/applications/sidebar/tabs/actorDirectory.mjs
@@ -47,7 +47,7 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs.
_getEntryContextOptions() {
const options = super._getEntryContextOptions();
options.push({
- name: 'DAGGERHEART.UI.Sidebar.actorDirectory.duplicateToNewTier',
+ name: 'Duplicate To New Tier',
icon: ``,
condition: li => {
const actor = game.actors.get(li.dataset.entryId);
@@ -57,27 +57,12 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs.
const actor = game.actors.get(li.dataset.entryId);
if (!actor) throw new Error('Unexpected missing actor');
- const tiers = [1, 2, 3, 4].filter(t => t !== actor.system.tier);
- const content = document.createElement('div');
- const select = document.createElement('select');
- select.name = 'tier';
- select.append(
- ...tiers.map(t => {
- const option = document.createElement('option');
- option.value = t;
- option.textContent = game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${t}`);
- return option;
- })
- );
- content.append(select);
-
const tier = await foundry.applications.api.Dialog.input({
- classes: ['dh-style', 'dialog'],
- window: { title: 'DAGGERHEART.UI.Sidebar.actorDirectory.pickTierTitle' },
- content,
+ window: { title: 'Pick a new tier for this adversary' },
+ content: '',
ok: {
label: 'Create Adversary',
- callback: (event, button, dialog) => Number(button.form.elements.tier.value)
+ callback: (event, button, dialog) => Math.clamp(button.form.elements.tier.valueAsNumber, 1, 4)
}
});
diff --git a/module/config/actorConfig.mjs b/module/config/actorConfig.mjs
index ac55117a..c455e7d7 100644
--- a/module/config/actorConfig.mjs
+++ b/module/config/actorConfig.mjs
@@ -747,22 +747,18 @@ export const adversaryScalingData = {
}
};
-/**
- * Scaling data used for an adversary's damage.
- * Tier 4 is missing certain adversary types and therefore skews upwards.
- * We manually set tier 4 data to hopefully lead to better results
- */
+/** Scaling data used for an adversary's damage */
export const adversaryExpectedDamage = {
basic: {
- 1: { mean: 7.321428571428571, deviation: 1.962519002770912 },
- 2: { mean: 12.444444444444445, deviation: 2.0631069425529676 },
- 3: { mean: 15.722222222222221, deviation: 2.486565208464823 },
- 4: { mean: 26, deviation: 5.2 }
+ 1: { median: 7.5, deviation: 1 },
+ 2: { median: 13, deviation: 2 },
+ 3: { median: 15.5, deviation: 1.5 },
+ 4: { median: 27, deviation: 3 }
},
minion: {
- 1: { mean: 2.142857142857143, deviation: 1.0690449676496976 },
- 2: { mean: 5, deviation: 0.816496580927726 },
- 3: { mean: 6.5, deviation: 2.1213203435596424 },
- 4: { mean: 11, deviation: 1 }
+ 1: { median: 2, deviation: 1 },
+ 2: { median: 5, deviation: 0.5 },
+ 3: { median: 6.5, deviation: 1.5 },
+ 4: { median: 11, deviation: 1 }
}
};
diff --git a/module/data/actor/adversary.mjs b/module/data/actor/adversary.mjs
index f9a2f256..11c72daa 100644
--- a/module/data/actor/adversary.mjs
+++ b/module/data/actor/adversary.mjs
@@ -193,6 +193,7 @@ export default class DhpAdversary extends BaseDataActor {
adjustForTier(tier) {
const source = this.parent.toObject(true);
+ console.log('Actors and source', this.parent, source);
/** @type {(2 | 3 | 4)[]} */
const tiers = new Array(Math.abs(tier - this.tier))
@@ -213,7 +214,7 @@ export default class DhpAdversary extends BaseDataActor {
source.system.attack.roll.bonus += scale * entry.attack;
}
- // Get the mean and standard deviation of expected damage in the previous and new tier
+ // Get the median and median absolute deviation of expected damage in the previous and new tier
const expectedDamageData = adversaryExpectedDamage[source.system.type] ?? adversaryExpectedDamage.basic;
const currentDamageRange = { tier: source.system.tier, ...expectedDamageData[source.system.tier] };
const newDamageRange = { tier, ...expectedDamageData[tier] };
@@ -273,8 +274,8 @@ export default class DhpAdversary extends BaseDataActor {
const dieSizes = ['d4', 'd6', 'd8', 'd10', 'd12', 'd20'];
const steps = newDamageRange.tier - currentDamageRange.tier;
const increasing = steps > 0;
- const deviation = (previousExpected - currentDamageRange.mean) / currentDamageRange.deviation;
- const expected = newDamageRange.mean + newDamageRange.deviation * deviation;
+ const deviation = (previousExpected - currentDamageRange.median) / currentDamageRange.deviation;
+ const expected = newDamageRange.median + newDamageRange.deviation * deviation;
const value = hitPointParts[0].value;
const getExpectedDie = () => Number(value.dice.replace('d', '')) / 2;
diff --git a/tools/analyze-damage.mjs b/tools/analyze-damage.mjs
index 6d5da3de..e94675b4 100644
--- a/tools/analyze-damage.mjs
+++ b/tools/analyze-damage.mjs
@@ -26,16 +26,8 @@ for (const basefile of await fs.readdir(adversariesDirectory)) {
});
}
-const adversaryTypes = new Set(allData.map(a => a.adversaryType));
-for (const type of [...adversaryTypes].toSorted()) {
- const perTier = Object.groupBy(allData.filter(a => a.adversaryType === type), a => a.tier);
- console.log(`${type} per Tier: ${[1, 2, 3, 4].map(t => perTier[t]?.length ?? 0).join(" ")}`)
-}
-
const result = {
basic: compileData(allData.filter(d => d.adversaryType !== "minion")),
- solos_and_bruisers: compileData(allData.filter(d => ["solo", "bruiser"].includes(d.adversaryType))),
- leader_and_ranged: compileData(allData.filter(d => ["leader", "ranged"].includes(d.adversaryType))),
minion: compileData(allData.filter(d => d.adversaryType === "minion")),
};
@@ -47,47 +39,23 @@ function compileData(entries) {
const results = {};
for (const tier of [1, 2, 3, 4]) {
const tierEntries = entries.filter(e => e.tier === tier);
- const allDamage = removeOutliers(tierEntries.map(d => d.damage).sort((a, b) => a - b));
- const mean = getMean(allDamage);
- if (tier === 4) console.log(allDamage);
+ const allDamage = tierEntries.map(d => d.damage).sort((a, b) => a - b);
+ const median = getMedian(allDamage);
+ const residuals = allDamage.map(d => Math.abs(d - median)).sort((a, b) => a - b);
results[tier] = {
- mean,
- deviation: getStandardDeviation(allDamage, { mean }),
+ medianDamage: median,
+ damageDeviation: getMedian(residuals),
};
}
return results;
}
-function removeOutliers(data) {
- if (data.length <= 4) return data;
- const startIdx = Math.floor(data.length * 0.25);
- const endIdx = Math.ceil(data.length * 0.75);
- const iqrBound = (data[endIdx] - data[startIdx]) * 1.25;
- return data.filter((d) => d >= data[startIdx] - iqrBound && d <= data[endIdx] + iqrBound);
-}
-
function getMedian(numbers) {
- numbers = numbers.toSorted((a, b) => a - b);
const medianIdx = numbers.length / 2;
return medianIdx % 1 ? numbers[Math.floor(medianIdx)] : (numbers[medianIdx] + numbers[medianIdx - 1]) / 2;
}
-function getMean(numbers) {
- if (numbers.length === 0) return NaN;
- return numbers.reduce((r, a) => r + a, 0) / numbers.length;
-}
-
-function getMedianAverageDeviation(numbers, { median }) {
- const residuals = allDamage.map(d => Math.abs(d - median));
- return getMedian(residuals);
-}
-
-function getStandardDeviation(numbers, { mean }) {
- const deviations = numbers.map((r) => r - mean);
- return Math.sqrt(deviations.reduce((r, d) => r + d * d, 0) / (numbers.length - 1));
-}
-
function parseDamage(damage) {
const formula = damage.parts
.filter(p => p.applyTo === 'hitPoints')