mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-03-07 06:26:13 +01:00
Use standard deviation instead and change dialog type
This commit is contained in:
parent
5a95744b6f
commit
57cd7c0679
5 changed files with 72 additions and 23 deletions
|
|
@ -2849,7 +2849,9 @@
|
|||
"tier": "Tier {tier} {type}",
|
||||
"character": "Level {level} Character",
|
||||
"companion": "Level {level} - {partner}",
|
||||
"companionNoPartner": "No Partner"
|
||||
"companionNoPartner": "No Partner",
|
||||
"duplicateToNewTier": "Duplicate to New Tier",
|
||||
"pickTierTitle": "Pick a new tier for this adversary"
|
||||
},
|
||||
"daggerheartMenu": {
|
||||
"title": "Daggerheart Menu",
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs.
|
|||
_getEntryContextOptions() {
|
||||
const options = super._getEntryContextOptions();
|
||||
options.push({
|
||||
name: 'Duplicate To New Tier',
|
||||
name: 'DAGGERHEART.UI.Sidebar.actorDirectory.duplicateToNewTier',
|
||||
icon: `<i class="fa-solid fa-arrow-trend-up" inert></i>`,
|
||||
condition: li => {
|
||||
const actor = game.actors.get(li.dataset.entryId);
|
||||
|
|
@ -57,12 +57,24 @@ 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({
|
||||
window: { title: 'Pick a new tier for this adversary' },
|
||||
content: '<input name="tier" type="number" min="1" max="4" step="1" autofocus>',
|
||||
window: { title: 'DAGGERHEART.UI.Sidebar.actorDirectory.pickTierTitle' },
|
||||
content,
|
||||
ok: {
|
||||
label: 'Create Adversary',
|
||||
callback: (event, button, dialog) => Math.clamp(button.form.elements.tier.valueAsNumber, 1, 4)
|
||||
callback: (event, button, dialog) => Number(button.form.elements.tier.value)
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -747,18 +747,22 @@ export const adversaryScalingData = {
|
|||
}
|
||||
};
|
||||
|
||||
/** Scaling data used for an adversary's damage */
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export const adversaryExpectedDamage = {
|
||||
basic: {
|
||||
1: { median: 7.5, deviation: 1 },
|
||||
2: { median: 13, deviation: 2 },
|
||||
3: { median: 15.5, deviation: 1.5 },
|
||||
4: { median: 27, deviation: 3 }
|
||||
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 }
|
||||
},
|
||||
minion: {
|
||||
1: { median: 2, deviation: 1 },
|
||||
2: { median: 5, deviation: 0.5 },
|
||||
3: { median: 6.5, deviation: 1.5 },
|
||||
4: { median: 11, deviation: 1 }
|
||||
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 }
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -193,7 +193,6 @@ 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))
|
||||
|
|
@ -214,7 +213,7 @@ export default class DhpAdversary extends BaseDataActor {
|
|||
source.system.attack.roll.bonus += scale * entry.attack;
|
||||
}
|
||||
|
||||
// Get the median and median absolute deviation of expected damage in the previous and new tier
|
||||
// Get the mean and standard 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] };
|
||||
|
|
@ -274,8 +273,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.median) / currentDamageRange.deviation;
|
||||
const expected = newDamageRange.median + newDamageRange.deviation * deviation;
|
||||
const deviation = (previousExpected - currentDamageRange.mean) / currentDamageRange.deviation;
|
||||
const expected = newDamageRange.mean + newDamageRange.deviation * deviation;
|
||||
|
||||
const value = hitPointParts[0].value;
|
||||
const getExpectedDie = () => Number(value.dice.replace('d', '')) / 2;
|
||||
|
|
|
|||
|
|
@ -26,8 +26,16 @@ 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")),
|
||||
};
|
||||
|
||||
|
|
@ -39,23 +47,47 @@ function compileData(entries) {
|
|||
const results = {};
|
||||
for (const tier of [1, 2, 3, 4]) {
|
||||
const tierEntries = entries.filter(e => e.tier === tier);
|
||||
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);
|
||||
const allDamage = removeOutliers(tierEntries.map(d => d.damage).sort((a, b) => a - b));
|
||||
const mean = getMean(allDamage);
|
||||
if (tier === 4) console.log(allDamage);
|
||||
results[tier] = {
|
||||
medianDamage: median,
|
||||
damageDeviation: getMedian(residuals),
|
||||
mean,
|
||||
deviation: getStandardDeviation(allDamage, { mean }),
|
||||
};
|
||||
}
|
||||
|
||||
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')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue