diff --git a/lang/en.json b/lang/en.json index 055e49c6..4355140e 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2979,9 +2979,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..d40443a0 100644 --- a/module/applications/sidebar/tabs/actorDirectory.mjs +++ b/module/applications/sidebar/tabs/actorDirectory.mjs @@ -43,54 +43,4 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs. event.dataTransfer.setDragImage(preview, w / 2, h / 2); } } - - _getEntryContextOptions() { - const options = super._getEntryContextOptions(); - options.push({ - name: 'DAGGERHEART.UI.Sidebar.actorDirectory.duplicateToNewTier', - icon: ``, - condition: li => { - const actor = game.actors.get(li.dataset.entryId); - return actor?.type === 'adversary' && actor.system.type !== 'social'; - }, - callback: async li => { - 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, - ok: { - label: 'Create Adversary', - callback: (event, button, dialog) => Number(button.form.elements.tier.value) - } - }); - - if (tier === actor.system.tier) { - ui.notifications.warn('This actor is already at this tier'); - } else if (tier) { - const source = actor.system.adjustForTier(tier); - await Actor.create(source); - ui.notifications.info(`Tier ${tier} ${actor.name} created`); - } - } - }); - - return options; - } } diff --git a/module/config/actorConfig.mjs b/module/config/actorConfig.mjs index ac55117a..fdef7d03 100644 --- a/module/config/actorConfig.mjs +++ b/module/config/actorConfig.mjs @@ -494,275 +494,3 @@ export const subclassFeatureLabels = { 2: 'DAGGERHEART.ITEMS.DomainCard.specializationTitle', 3: 'DAGGERHEART.ITEMS.DomainCard.masteryTitle' }; - -/** - * @typedef {Object} TierData - * @property {number} difficulty - * @property {number} majorThreshold - * @property {number} severeThreshold - * @property {number} hp - * @property {number} stress - * @property {number} attack - * @property {number[]} damage - */ - -/** - * @type {Record} - * Scaling data used to change an adversary's tier. Each rank is applied incrementally. - */ -export const adversaryScalingData = { - bruiser: { - 2: { - difficulty: 2, - majorThreshold: 5, - severeThreshold: 10, - hp: 1, - stress: 2, - attack: 2, - }, - 3: { - difficulty: 2, - majorThreshold: 7, - severeThreshold: 15, - hp: 1, - stress: 0, - attack: 2, - }, - 4: { - difficulty: 2, - majorThreshold: 12, - severeThreshold: 25, - hp: 1, - stress: 0, - attack: 2, - } - }, - horde: { - 2: { - difficulty: 2, - majorThreshold: 5, - severeThreshold: 8, - hp: 2, - stress: 0, - attack: 0, - }, - 3: { - difficulty: 2, - majorThreshold: 5, - severeThreshold: 12, - hp: 0, - stress: 1, - attack: 1, - }, - 4: { - difficulty: 2, - majorThreshold: 10, - severeThreshold: 15, - hp: 2, - stress: 0, - attack: 0, - } - }, - leader: { - 2: { - difficulty: 2, - majorThreshold: 6, - severeThreshold: 10, - hp: 0, - stress: 0, - attack: 1, - }, - 3: { - difficulty: 2, - majorThreshold: 6, - severeThreshold: 15, - hp: 1, - stress: 0, - attack: 2, - }, - 4: { - difficulty: 2, - majorThreshold: 12, - severeThreshold: 25, - hp: 1, - stress: 1, - attack: 3, - } - }, - minion: { - 2: { - difficulty: 2, - majorThreshold: 0, - severeThreshold: 0, - hp: 0, - stress: 0, - attack: 1, - }, - 3: { - difficulty: 2, - majorThreshold: 0, - severeThreshold: 0, - hp: 0, - stress: 1, - attack: 1, - }, - 4: { - difficulty: 2, - majorThreshold: 0, - severeThreshold: 0, - hp: 0, - stress: 0, - attack: 1, - } - }, - ranged: { - 2: { - difficulty: 2, - majorThreshold: 3, - severeThreshold: 6, - hp: 1, - stress: 0, - attack: 1, - }, - 3: { - difficulty: 2, - majorThreshold: 7, - severeThreshold: 14, - hp: 1, - stress: 1, - attack: 2, - }, - 4: { - difficulty: 2, - majorThreshold: 5, - severeThreshold: 10, - hp: 1, - stress: 1, - attack: 1, - } - }, - skulk: { - 2: { - difficulty: 2, - majorThreshold: 3, - severeThreshold: 8, - hp: 1, - stress: 1, - attack: 1, - }, - 3: { - difficulty: 2, - majorThreshold: 8, - severeThreshold: 12, - hp: 1, - stress: 1, - attack: 1, - }, - 4: { - difficulty: 2, - majorThreshold: 8, - severeThreshold: 10, - hp: 1, - stress: 1, - attack: 1, - } - }, - solo: { - 2: { - difficulty: 2, - majorThreshold: 5, - severeThreshold: 10, - hp: 0, - stress: 1, - attack: 2, - }, - 3: { - difficulty: 2, - majorThreshold: 7, - severeThreshold: 15, - hp: 2, - stress: 1, - attack: 2, - }, - 4: { - difficulty: 2, - majorThreshold: 12, - severeThreshold: 25, - hp: 0, - stress: 1, - attack: 3, - } - }, - standard: { - 2: { - difficulty: 2, - majorThreshold: 3, - severeThreshold: 8, - hp: 0, - stress: 0, - attack: 1, - }, - 3: { - difficulty: 2, - majorThreshold: 7, - severeThreshold: 15, - hp: 1, - stress: 1, - attack: 1, - }, - 4: { - difficulty: 2, - majorThreshold: 10, - severeThreshold: 15, - hp: 0, - stress: 1, - attack: 1, - } - }, - support: { - 2: { - difficulty: 2, - majorThreshold: 3, - severeThreshold: 8, - hp: 1, - stress: 1, - attack: 1, - }, - 3: { - difficulty: 2, - majorThreshold: 7, - severeThreshold: 12, - hp: 0, - stress: 0, - attack: 1, - }, - 4: { - difficulty: 2, - majorThreshold: 8, - severeThreshold: 10, - hp: 1, - stress: 1, - attack: 1, - } - } -}; - -/** - * 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: { 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: { mean: 2.142857142857143, deviation: 1.0690449676496976 }, - 2: { mean: 5, deviation: 0.816496580927726 }, - 3: { mean: 6.5, deviation: 2.1213203435596424 }, - 4: { mean: 11, deviation: 1 } - } -}; diff --git a/module/data/action/attackAction.mjs b/module/data/action/attackAction.mjs index 60112c40..7be7461d 100644 --- a/module/data/action/attackAction.mjs +++ b/module/data/action/attackAction.mjs @@ -34,20 +34,6 @@ export default class DHAttackAction extends DHDamageAction { }; } - get damageFormula() { - const hitPointsPart = this.damage.parts.find(x => x.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id); - if (!hitPointsPart) return '0'; - - return hitPointsPart.value.getFormula(); - } - - get altDamageFormula() { - const hitPointsPart = this.damage.parts.find(x => x.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id); - if (!hitPointsPart) return '0'; - - return hitPointsPart.valueAlt.getFormula(); - } - async use(event, options) { const result = await super.use(event, options); if (!result.message) return; diff --git a/module/data/actor/adversary.mjs b/module/data/actor/adversary.mjs index d3844bcb..f2c38090 100644 --- a/module/data/actor/adversary.mjs +++ b/module/data/actor/adversary.mjs @@ -2,8 +2,6 @@ import DHAdversarySettings from '../../applications/sheets-configs/adversary-set import { ActionField } from '../fields/actionField.mjs'; import BaseDataActor, { commonActorRules } from './base.mjs'; import { resourceField, bonusField } from '../fields/actorField.mjs'; -import { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs'; -import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs'; export default class DhpAdversary extends BaseDataActor { static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Adversary']; @@ -197,211 +195,4 @@ export default class DhpAdversary extends BaseDataActor { ]; return tags; } - - /** Returns source data for this actor adjusted to a new tier, which can be used to create a new actor. */ - adjustForTier(tier) { - const source = this.parent.toObject(true); - - /** @type {(2 | 3 | 4)[]} */ - const tiers = new Array(Math.abs(tier - this.tier)) - .fill(0) - .map((_, idx) => idx + Math.min(tier, this.tier) + 1); - if (tier < this.tier) tiers.reverse(); - const typeData = adversaryScalingData[source.system.type] ?? adversaryScalingData[source.system.standard]; - const tierEntries = tiers.map(t => ({ tier: t, ...typeData[t] })); - - // Apply simple tier changes - const scale = tier > this.tier ? 1 : -1; - for (const entry of tierEntries) { - source.system.difficulty += scale * entry.difficulty; - source.system.damageThresholds.major += scale * entry.majorThreshold; - source.system.damageThresholds.severe += scale * entry.severeThreshold; - source.system.resources.hitPoints.max += scale * entry.hp; - source.system.resources.stress.max += scale * entry.stress; - source.system.attack.roll.bonus += scale * entry.attack; - } - - // Get the mean and standard deviation of expected damage in the previous and new tier - // The data we have is for attack scaling, but we reuse this for action scaling later - const expectedDamageData = adversaryExpectedDamage[source.system.type] ?? adversaryExpectedDamage.basic; - const damageMeta = { - currentDamageRange: { tier: source.system.tier, ...expectedDamageData[source.system.tier] }, - newDamageRange: { tier, ...expectedDamageData[tier] }, - type: 'attack' - }; - - // Update damage of base attack - try { - this.#adjustActionDamage(source.system.attack, damageMeta); - } catch (err) { - ui.notifications.warn('Failed to convert attack damage of adversary'); - console.error(err); - } - - // Update damage of each item action, making sure to also update the description if possible - const damageRegex = /@Damage\[([^\[\]]*)\]({[^}]*})?/g; - for (const item of source.items) { - // Replace damage inlines with new formulas - for (const withDescription of [item.system, ...Object.values(item.system.actions)]) { - withDescription.description = withDescription.description.replace(damageRegex, (match, inner) => { - const { value: formula } = parseInlineParams(inner); - if (!formula || !type) return match; - - try { - const adjusted = this.#calculateAdjustedDamage(formula, { ...damageMeta, type: 'action' }); - const newFormula = [ - adjusted.diceQuantity ? `${adjusted.diceQuantity}d${adjusted.faces}` : null, - adjusted.bonus - ] - .filter(p => !!p) - .join('+'); - return match.replace(formula, newFormula); - } catch { - return match; - } - }); - } - - // Update damage in item actions - for (const action of Object.values(item.system.actions)) { - if (!action.damage) continue; - - // Parse damage, and convert all formula matches in the descriptions to the new damage - try { - const result = this.#adjustActionDamage(action, { ...damageMeta, type: 'action' }); - for (const { previousFormula, formula } of Object.values(result)) { - const oldFormulaRegexp = new RegExp( - previousFormula.replace(' ', '').replace('+', '(?:\\s)?\\+(?:\\s)?') - ); - item.system.description = item.system.description.replace(oldFormulaRegexp, formula); - action.description = action.description.replace(oldFormulaRegexp, formula); - } - } catch (err) { - ui.notifications.warn(`Failed to convert action damage for item ${item.name}`); - console.error(err); - } - } - } - - // Finally set the tier of the source data, now that everything is complete - source.system.tier = tier; - return source; - } - - /** - * Converts a damage object to a new damage range - * @returns {{ diceQuantity: number; faces: number; bonus: number }} the adjusted result as a combined term - * @throws error if the formula is the wrong type - */ - #calculateAdjustedDamage(formula, { currentDamageRange, newDamageRange, type }) { - const terms = parseTermsFromSimpleFormula(formula); - const flatTerms = terms.filter(t => t.diceQuantity === 0); - const diceTerms = terms.filter(t => t.diceQuantity > 0); - if (flatTerms.length > 1 || diceTerms.length > 1) { - throw new Error('invalid formula for conversion'); - } - const value = { - ...(diceTerms[0] ?? { diceQuantity: 0, faces: 1 }), - bonus: flatTerms[0]?.bonus ?? 0 - }; - const previousExpected = calculateExpectedValue(value); - if (previousExpected === 0) return value; // nothing to do - - const dieSizes = [4, 6, 8, 10, 12, 20]; - const steps = newDamageRange.tier - currentDamageRange.tier; - const increasing = steps > 0; - const deviation = (previousExpected - currentDamageRange.mean) / currentDamageRange.deviation; - const expected = Math.max(1, newDamageRange.mean + newDamageRange.deviation * deviation); - - // If this was just a flat number, convert to the expected damage and exit - if (value.diceQuantity === 0) { - value.bonus = Math.round(expected); - return value; - } - - const getExpectedDie = () => calculateExpectedValue({ diceQuantity: 1, faces: value.faces }) || 1; - const getBaseAverage = () => calculateExpectedValue({ ...value, bonus: 0 }); - - // Check the number of base overages over the expected die. In the end, if the bonus inflates too much, we add a die - const baseOverages = Math.floor(value.bonus / getExpectedDie()); - - // Prestep. Change number of dice for attacks, bump up/down for actions - // We never bump up to d20, though we might bump down from it - if (type === 'attack') { - const minimum = increasing ? value.diceQuantity : 0; - value.diceQuantity = Math.max(minimum, newDamageRange.tier); - } else { - const currentIdx = dieSizes.indexOf(value.faces); - value.faces = dieSizes[Math.clamp(currentIdx + steps, 0, 4)]; - } - - value.bonus = Math.round(expected - getBaseAverage()); - - // Attempt to handle negative values. - // If we can do it with only step downs, do so. Otherwise remove tier dice, and try again - if (value.bonus < 0) { - let stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity); - const currentIdx = dieSizes.indexOf(value.faces); - - // If step downs alone don't suffice, change the flat modifier, then calculate steps required again - // If this isn't sufficient, the result will be slightly off. This is unlikely to happen - if (type !== 'attack' && stepsRequired > currentIdx && value.diceQuantity > 0) { - value.diceQuantity -= increasing ? 1 : Math.abs(steps); - value.bonus = Math.round(expected - getBaseAverage()); - if (value.bonus >= 0) return value; // complete - } - - stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity); - value.faces = dieSizes[Math.max(0, currentIdx - stepsRequired)]; - value.bonus = Math.max(0, Math.round(expected - getBaseAverage())); - } - - // If value is really high, we add a number of dice based on the number of overages - // This attempts to preserve a similar amount of variance when increasing an action - const overagesToRemove = Math.floor(value.bonus / getExpectedDie()) - baseOverages; - if (type !== 'attack' && increasing && overagesToRemove > 0) { - value.diceQuantity += overagesToRemove; - value.bonus = Math.round(expected - getBaseAverage()); - } - - return value; - } - - /** - * Updates damage to reflect a specific value. - * @throws if damage structure is invalid for conversion - * @returns the converted formula and value as a simplified term - */ - #adjustActionDamage(action, damageMeta) { - // The current algorithm only returns a value if there is a single damage part - const hpDamageParts = action.damage.parts.filter(d => d.applyTo === 'hitPoints'); - if (hpDamageParts.length !== 1) throw new Error('incorrect number of hp parts'); - - const result = {}; - for (const property of ['value', 'valueAlt']) { - const data = hpDamageParts[0][property]; - const previousFormula = data.custom.enabled - ? data.custom.formula - : [data.flatMultiplier ? `${data.flatMultiplier}${data.dice}` : 0, data.bonus ?? 0] - .filter(p => !!p) - .join('+'); - const value = this.#calculateAdjustedDamage(previousFormula, damageMeta); - const formula = [value.diceQuantity ? `${value.diceQuantity}d${value.faces}` : null, value.bonus] - .filter(p => !!p) - .join('+'); - if (value.diceQuantity) { - data.custom.enabled = false; - data.bonus = value.bonus; - data.dice = `d${value.faces}`; - data.flatMultiplier = value.diceQuantity; - } else if (!value.diceQuantity) { - data.custom.enabled = true; - data.custom.formula = formula; - } - - result[property] = { previousFormula, formula, value }; - } - - return result; - } } diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 607ce94e..c0dd45bd 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -495,49 +495,3 @@ export function htmlToText(html) { return tempDivElement.textContent || tempDivElement.innerText || ''; } - -/** - * Given a simple flavor-less formula with only +/- operators, returns a list of damage partial terms. - * All subtracted terms become negative terms. - * If there are no dice, it returns 0d1 for that term. - */ -export function parseTermsFromSimpleFormula(formula) { - const roll = formula instanceof Roll ? formula : new Roll(formula); - - // Parse from right to left so that when we hit an operator, we already have the term. - return roll.terms.reduceRight((result, term) => { - // Ignore + terms, we assume + by default - if (term.expression === ' + ') return result; - - // - terms modify the last term we parsed - if (term.expression === ' - ') { - const termToModify = result[0]; - if (termToModify) { - if (termToModify.bonus) termToModify.bonus *= -1; - if (termToModify.dice) termToModify.dice *= -1; - } - return result; - } - - result.unshift({ - bonus: term instanceof foundry.dice.terms.NumericTerm ? term.number : 0, - diceQuantity: term instanceof foundry.dice.terms.Die ? term.number : 0, - faces: term.faces ?? 1 - }); - - return result; - }, []); -} - -/** - * Calculates the expectede value from a formula or the results of parseTermsFromSimpleFormula. - * @returns {number} the average result of rolling the given dice - */ -export function calculateExpectedValue(formulaOrTerms) { - const terms = Array.isArray(formulaOrTerms) - ? formulaOrTerms - : typeof formulaOrTerms === 'string' - ? parseTermsFromSimpleFormula(formulaOrTerms) - : [formulaOrTerms]; - return terms.reduce((r, t) => r + (t.bonus ?? 0) + (t.diceQuantity ? (t.diceQuantity * (t.faces + 1)) / 2 : 0), 0); -} diff --git a/src/packs/adversaries/adversary_Apprentice_Assassin_vNIbYQ4YSzNf0WPE.json b/src/packs/adversaries/adversary_Apprentice_Assassin_vNIbYQ4YSzNf0WPE.json index 23f1f339..3f31ff76 100644 --- a/src/packs/adversaries/adversary_Apprentice_Assassin_vNIbYQ4YSzNf0WPE.json +++ b/src/packs/adversaries/adversary_Apprentice_Assassin_vNIbYQ4YSzNf0WPE.json @@ -246,7 +246,7 @@ "name": "Group Attack", "type": "feature", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 4 physical damage each. Combine this damage.

", "resource": null, "actions": { "vgguNWz8vG8aoLXR": { diff --git a/src/packs/adversaries/adversary_Archer_Squadron_0ts6CGd93lLqGZI5.json b/src/packs/adversaries/adversary_Archer_Squadron_0ts6CGd93lLqGZI5.json index 5b15bc09..55229040 100644 --- a/src/packs/adversaries/adversary_Archer_Squadron_0ts6CGd93lLqGZI5.json +++ b/src/packs/adversaries/adversary_Archer_Squadron_0ts6CGd93lLqGZI5.json @@ -218,10 +218,10 @@ }, "items": [ { - "name": "Horde", + "name": "Horde (1d6+3)", "type": "feature", "system": { - "description": "

When the @Lookup[@name] has marked half or more of their HP, their standard attack deals @Lookup[@system.attack.altDamageFormula] physical damage instead.

", + "description": "

When the @Lookup[@name] has marked half or more of their HP, their standard attack deals 1d6+3 physical damage instead.

", "resource": null, "actions": {}, "originItemType": null, diff --git a/src/packs/adversaries/adversary_Conscript_99TqczuQipBmaB8i.json b/src/packs/adversaries/adversary_Conscript_99TqczuQipBmaB8i.json index 35c43a3b..c5b4357d 100644 --- a/src/packs/adversaries/adversary_Conscript_99TqczuQipBmaB8i.json +++ b/src/packs/adversaries/adversary_Conscript_99TqczuQipBmaB8i.json @@ -239,7 +239,7 @@ "name": "Group Attack", "type": "feature", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 6 physical damage each. Combine this damage.

", "resource": null, "actions": { "cbAvPSIhwBMBTI3D": { diff --git a/src/packs/adversaries/adversary_Cult_Initiate_zx99sOGTXicP4SSD.json b/src/packs/adversaries/adversary_Cult_Initiate_zx99sOGTXicP4SSD.json index a0c0713d..0e14a661 100644 --- a/src/packs/adversaries/adversary_Cult_Initiate_zx99sOGTXicP4SSD.json +++ b/src/packs/adversaries/adversary_Cult_Initiate_zx99sOGTXicP4SSD.json @@ -239,7 +239,7 @@ "name": "Group Attack", "type": "feature", "system": { - "description": "

Spend a Fear to choose a target and spotlight all Cult @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all Cult @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 5 physical damage each. Combine this damage.

", "resource": null, "actions": { "EH1preaTWBD4rOvx": { diff --git a/src/packs/adversaries/adversary_Demonic_Hound_Pack_NoRZ1PqB8N5wcIw0.json b/src/packs/adversaries/adversary_Demonic_Hound_Pack_NoRZ1PqB8N5wcIw0.json index 7482c734..2947b7a1 100644 --- a/src/packs/adversaries/adversary_Demonic_Hound_Pack_NoRZ1PqB8N5wcIw0.json +++ b/src/packs/adversaries/adversary_Demonic_Hound_Pack_NoRZ1PqB8N5wcIw0.json @@ -224,10 +224,10 @@ }, "items": [ { - "name": "Horde", + "name": "Horde (2d4+1)", "type": "feature", "system": { - "description": "

When the @Lookup[@name] has marked half or more of their HP, their standard attack deals @Lookup[@system.attack.altDamageFormula] physical damage instead.

", + "description": "

When the @Lookup[@name] has marked half or more of their HP, their standard attack deals 2d4+1 physical damage instead.

", "resource": null, "actions": {}, "originItemType": null, diff --git a/src/packs/adversaries/adversary_Electric_Eels_TLzY1nDw0Bu9Ud40.json b/src/packs/adversaries/adversary_Electric_Eels_TLzY1nDw0Bu9Ud40.json index 9386944f..7b41b9e5 100644 --- a/src/packs/adversaries/adversary_Electric_Eels_TLzY1nDw0Bu9Ud40.json +++ b/src/packs/adversaries/adversary_Electric_Eels_TLzY1nDw0Bu9Ud40.json @@ -218,10 +218,10 @@ }, "items": [ { - "name": "Horde", + "name": "Horde (2d4+1)", "type": "feature", "system": { - "description": "

When the @Lookup[@name] have marked half or more of their HP, their standard attack deals @Lookup[@system.attack.altDamageFormula] physical damage instead.

", + "description": "

When the @Lookup[@name] have marked half or more of their HP, their standard attack deals 2d4+1 physical damage instead.

", "resource": null, "actions": {}, "originItemType": null, diff --git a/src/packs/adversaries/adversary_Elemental_Spark_P7h54ZePFPHpYwvB.json b/src/packs/adversaries/adversary_Elemental_Spark_P7h54ZePFPHpYwvB.json index 5c25f63e..b17cae1c 100644 --- a/src/packs/adversaries/adversary_Elemental_Spark_P7h54ZePFPHpYwvB.json +++ b/src/packs/adversaries/adversary_Elemental_Spark_P7h54ZePFPHpYwvB.json @@ -239,7 +239,7 @@ "name": "Group Attack", "type": "feature", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 5 physical damage each. Combine this damage.

", "resource": null, "actions": { "vXHZVb0Y7Hqu3uso": { diff --git a/src/packs/adversaries/adversary_Fallen_Shock_Troop_OsLG2BjaEdTZUJU9.json b/src/packs/adversaries/adversary_Fallen_Shock_Troop_OsLG2BjaEdTZUJU9.json index 931e4c0a..163c61f7 100644 --- a/src/packs/adversaries/adversary_Fallen_Shock_Troop_OsLG2BjaEdTZUJU9.json +++ b/src/packs/adversaries/adversary_Fallen_Shock_Troop_OsLG2BjaEdTZUJU9.json @@ -317,7 +317,7 @@ "name": "Group Attack", "type": "feature", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 12 physical damage each. Combine this damage.

", "resource": null, "actions": { "QHNRSEQmqOcaoXq4": { diff --git a/src/packs/adversaries/adversary_Giant_Mosquitoes_IIWV4ysJPFPnTP7W.json b/src/packs/adversaries/adversary_Giant_Mosquitoes_IIWV4ysJPFPnTP7W.json index fbb30d40..54f12efa 100644 --- a/src/packs/adversaries/adversary_Giant_Mosquitoes_IIWV4ysJPFPnTP7W.json +++ b/src/packs/adversaries/adversary_Giant_Mosquitoes_IIWV4ysJPFPnTP7W.json @@ -229,7 +229,7 @@ "_id": "9RduwBLYcBaiouYk", "img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp", "system": { - "description": "

When the @Lookup[@name] have marked half or more of their HP, their standard attack deals @Lookup[@system.attack.altDamageFormula] physical damage instead.

", + "description": "

When the @Lookup[@name] have marked half or more of their HP, their standard attack deals 1d4+1 physical damage instead.

", "resource": null, "actions": {}, "originItemType": null, diff --git a/src/packs/adversaries/adversary_Giant_Rat_4PfLnaCrOcMdb4dK.json b/src/packs/adversaries/adversary_Giant_Rat_4PfLnaCrOcMdb4dK.json index d1df6b57..d4655880 100644 --- a/src/packs/adversaries/adversary_Giant_Rat_4PfLnaCrOcMdb4dK.json +++ b/src/packs/adversaries/adversary_Giant_Rat_4PfLnaCrOcMdb4dK.json @@ -248,7 +248,7 @@ "_id": "fsaBlCjTdq1jM23G", "img": "icons/creatures/abilities/tail-strike-bone-orange.webp", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 1 physical damage each. Combine this damage.

", "resource": null, "actions": { "q8chow47nQLR9qeF": { diff --git a/src/packs/adversaries/adversary_Giant_Recruit_5s8wSvpyC5rxY5aD.json b/src/packs/adversaries/adversary_Giant_Recruit_5s8wSvpyC5rxY5aD.json index ebdea711..75da96b2 100644 --- a/src/packs/adversaries/adversary_Giant_Recruit_5s8wSvpyC5rxY5aD.json +++ b/src/packs/adversaries/adversary_Giant_Recruit_5s8wSvpyC5rxY5aD.json @@ -239,7 +239,7 @@ "name": "Group Attack", "type": "feature", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 5 physical damage each. Combine this damage.

", "resource": null, "actions": { "DjbPQowW1OdBD9Zn": { diff --git a/src/packs/adversaries/adversary_Hallowed_Soldier_VENwg7xEFcYObjmT.json b/src/packs/adversaries/adversary_Hallowed_Soldier_VENwg7xEFcYObjmT.json index 96107752..cceed989 100644 --- a/src/packs/adversaries/adversary_Hallowed_Soldier_VENwg7xEFcYObjmT.json +++ b/src/packs/adversaries/adversary_Hallowed_Soldier_VENwg7xEFcYObjmT.json @@ -294,7 +294,7 @@ "name": "Group Attack", "type": "feature", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 10 physical damage each. Combine this damage.

", "resource": null, "actions": { "eo7J0v1B5zPHul1M": { diff --git a/src/packs/adversaries/adversary_Jagged_Knife_Lackey_C0OMQqV7pN6t7ouR.json b/src/packs/adversaries/adversary_Jagged_Knife_Lackey_C0OMQqV7pN6t7ouR.json index a52ec1c9..1a95bf87 100644 --- a/src/packs/adversaries/adversary_Jagged_Knife_Lackey_C0OMQqV7pN6t7ouR.json +++ b/src/packs/adversaries/adversary_Jagged_Knife_Lackey_C0OMQqV7pN6t7ouR.json @@ -248,7 +248,7 @@ "_id": "1k5TmQIAunM7Bv32", "img": "icons/creatures/abilities/tail-strike-bone-orange.webp", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name] within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name] within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 2 physical damage each. Combine this damage.

", "resource": null, "actions": { "aoQDb2m32NDxE6ZP": { diff --git a/src/packs/adversaries/adversary_Minor_Treant_G62k4oSkhkoXEs2D.json b/src/packs/adversaries/adversary_Minor_Treant_G62k4oSkhkoXEs2D.json index f05ba5fc..0f1e7ded 100644 --- a/src/packs/adversaries/adversary_Minor_Treant_G62k4oSkhkoXEs2D.json +++ b/src/packs/adversaries/adversary_Minor_Treant_G62k4oSkhkoXEs2D.json @@ -242,7 +242,7 @@ "_id": "K08WlZwGqzEo4idT", "img": "icons/creatures/abilities/tail-strike-bone-orange.webp", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 4 physical damage each. Combine this damage.

", "resource": null, "actions": { "xTMNAHcoErKuR6TZ": { diff --git a/src/packs/adversaries/adversary_Outer_Realms_Thrall_moJhHgKqTKPS2WYS.json b/src/packs/adversaries/adversary_Outer_Realms_Thrall_moJhHgKqTKPS2WYS.json index 276dd3ed..370182a5 100644 --- a/src/packs/adversaries/adversary_Outer_Realms_Thrall_moJhHgKqTKPS2WYS.json +++ b/src/packs/adversaries/adversary_Outer_Realms_Thrall_moJhHgKqTKPS2WYS.json @@ -239,7 +239,7 @@ "name": "Group Attack", "type": "feature", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 11 physical damage each. Combine this damage.

", "resource": null, "actions": { "tvQetauskZoHDR5y": { diff --git a/src/packs/adversaries/adversary_Pirate_Raiders_5YgEajn0wa4i85kC.json b/src/packs/adversaries/adversary_Pirate_Raiders_5YgEajn0wa4i85kC.json index 41f79b49..7d3733ce 100644 --- a/src/packs/adversaries/adversary_Pirate_Raiders_5YgEajn0wa4i85kC.json +++ b/src/packs/adversaries/adversary_Pirate_Raiders_5YgEajn0wa4i85kC.json @@ -229,7 +229,7 @@ "_id": "Q7DRbWjHl64CNwag", "img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp", "system": { - "description": "

When the @Lookup[@name] have marked half or more of their HP, their standard attack deals @Lookup[@system.attack.altDamageFormula] physical damage instead.

", + "description": "

When the @Lookup[@name] have marked half or more of their HP, their standard attack deals 1d4+1 physical damage instead.

", "resource": null, "actions": {}, "originItemType": null, diff --git a/src/packs/adversaries/adversary_Rotted_Zombie_gP3fWTLzSFnpA8EJ.json b/src/packs/adversaries/adversary_Rotted_Zombie_gP3fWTLzSFnpA8EJ.json index 7672961c..a9bf3a67 100644 --- a/src/packs/adversaries/adversary_Rotted_Zombie_gP3fWTLzSFnpA8EJ.json +++ b/src/packs/adversaries/adversary_Rotted_Zombie_gP3fWTLzSFnpA8EJ.json @@ -242,7 +242,7 @@ "_id": "R9vrwFNl5BD1YXJo", "img": "icons/creatures/abilities/tail-strike-bone-orange.webp", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 2 physical damage each. Combine this damage.

", "resource": null, "actions": { "DJBNtd3hWjwsjPwq": { diff --git a/src/packs/adversaries/adversary_Sellsword_bgreCaQ6ap2DVpCr.json b/src/packs/adversaries/adversary_Sellsword_bgreCaQ6ap2DVpCr.json index 514be8f5..e26b48eb 100644 --- a/src/packs/adversaries/adversary_Sellsword_bgreCaQ6ap2DVpCr.json +++ b/src/packs/adversaries/adversary_Sellsword_bgreCaQ6ap2DVpCr.json @@ -242,7 +242,7 @@ "_id": "CQZQiEiRH70Br5Ge", "img": "icons/creatures/abilities/tail-strike-bone-orange.webp", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 3 physical damage each. Combine this damage.

", "resource": null, "actions": { "ghgFZskDiizJDjcn": { diff --git a/src/packs/adversaries/adversary_Skeleton_Dredge_6l1a3Fazq8BoKIcc.json b/src/packs/adversaries/adversary_Skeleton_Dredge_6l1a3Fazq8BoKIcc.json index 4013d7fe..5a973b17 100644 --- a/src/packs/adversaries/adversary_Skeleton_Dredge_6l1a3Fazq8BoKIcc.json +++ b/src/packs/adversaries/adversary_Skeleton_Dredge_6l1a3Fazq8BoKIcc.json @@ -242,7 +242,7 @@ "_id": "wl9KKEpVWDBu62hU", "img": "icons/creatures/abilities/tail-strike-bone-orange.webp", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 1 physical damage each. Combine this damage.

", "resource": null, "actions": { "Sz55uB8xkoNytLwJ": { diff --git a/src/packs/adversaries/adversary_Swarm_of_Rats_qNgs3AbLyJrY19nt.json b/src/packs/adversaries/adversary_Swarm_of_Rats_qNgs3AbLyJrY19nt.json index 014b3dc6..33fe06d7 100644 --- a/src/packs/adversaries/adversary_Swarm_of_Rats_qNgs3AbLyJrY19nt.json +++ b/src/packs/adversaries/adversary_Swarm_of_Rats_qNgs3AbLyJrY19nt.json @@ -223,7 +223,7 @@ "_id": "9Zuu892SO5NmtI4w", "img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp", "system": { - "description": "

When the @Lookup[@name] has marked half or more of their HP, their standard attack deals @Lookup[@system.attack.altDamageFormula] physical damage instead.

", + "description": "

When the @Lookup[@name] has marked half or more of their HP, their standard attack deals 1d4+1 physical damage instead.

", "resource": null, "actions": {}, "originItemType": null, diff --git a/src/packs/adversaries/adversary_Tangle_Bramble_Swarm_PKSXFuaIHUCoH63A.json b/src/packs/adversaries/adversary_Tangle_Bramble_Swarm_PKSXFuaIHUCoH63A.json index 40297eb6..639fa956 100644 --- a/src/packs/adversaries/adversary_Tangle_Bramble_Swarm_PKSXFuaIHUCoH63A.json +++ b/src/packs/adversaries/adversary_Tangle_Bramble_Swarm_PKSXFuaIHUCoH63A.json @@ -254,12 +254,12 @@ }, "items": [ { - "name": "Horde", + "name": "Horde (1d4+2)", "type": "feature", "_id": "4dSzqtYvH385r9Ng", "img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp", "system": { - "description": "

When the @Lookup[@name] has marked half or more of their HP, their standard attack deals @Lookup[@system.attack.altDamageFormula] physical damage instead.

", + "description": "

When the @Lookup[@name] has marked half or more of their HP, their standard attack deals 1d4+2 physical damage instead.

", "resource": null, "actions": {}, "originItemType": null, diff --git a/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json b/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json index 33afaa3a..0f1ba28f 100644 --- a/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json +++ b/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json @@ -281,7 +281,7 @@ "_id": "WiobzuyvJ46zfsOv", "img": "icons/creatures/abilities/tail-strike-bone-orange.webp", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 2 physical damage each. Combine this damage.

", "resource": null, "actions": { "ZC5pKIb9N82vgMWu": { diff --git a/src/packs/adversaries/adversary_Treant_Sapling_o63nS0k3wHu6EgKP.json b/src/packs/adversaries/adversary_Treant_Sapling_o63nS0k3wHu6EgKP.json index c9ca695e..8959f78a 100644 --- a/src/packs/adversaries/adversary_Treant_Sapling_o63nS0k3wHu6EgKP.json +++ b/src/packs/adversaries/adversary_Treant_Sapling_o63nS0k3wHu6EgKP.json @@ -239,7 +239,7 @@ "name": "Group Attack", "type": "feature", "system": { - "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal @Lookup[@system.attack.damageFormula] physical damage each. Combine this damage.

", + "description": "

Spend a Fear to choose a target and spotlight all @Lookup[@name]s within Close range of them. Those Minions move into Melee range of the target and make one shared attack roll. On a success, they deal 8 physical damage each. Combine this damage.

", "resource": null, "actions": { "euP8VA4wvfsCpwN1": { diff --git a/src/packs/adversaries/adversary_Zombie_Legion_YhJrP7rTBiRdX5Fp.json b/src/packs/adversaries/adversary_Zombie_Legion_YhJrP7rTBiRdX5Fp.json index a6a488e9..1b2cce2a 100644 --- a/src/packs/adversaries/adversary_Zombie_Legion_YhJrP7rTBiRdX5Fp.json +++ b/src/packs/adversaries/adversary_Zombie_Legion_YhJrP7rTBiRdX5Fp.json @@ -218,10 +218,10 @@ }, "items": [ { - "name": "Horde", + "name": "Horde (2d6+5)", "type": "feature", "system": { - "description": "

When the @Lookup[@name] has marked half or more of their HP, their standard attack deals @Lookup[@system.attack.altDamageFormula] physical damage instead.

", + "description": "

When the @Lookup[@name] has marked half or more of their HP, their standard attack deals 2d6+5 physical damage instead.

", "resource": null, "actions": {}, "originItemType": null, diff --git a/src/packs/adversaries/adversary_Zombie_Pack_Nf0v43rtflV56V2T.json b/src/packs/adversaries/adversary_Zombie_Pack_Nf0v43rtflV56V2T.json index 017537ad..32519ac6 100644 --- a/src/packs/adversaries/adversary_Zombie_Pack_Nf0v43rtflV56V2T.json +++ b/src/packs/adversaries/adversary_Zombie_Pack_Nf0v43rtflV56V2T.json @@ -218,12 +218,12 @@ }, "items": [ { - "name": "Horde", + "name": "Horde (1d4+2)", "type": "feature", "_id": "nNJGAhWu0IuS2ybn", "img": "icons/creatures/magical/humanoid-silhouette-aliens-green.webp", "system": { - "description": "

When the @Lookup[@name] have marked half or more of their HP, their standard attack deals @Lookup[@system.attack.altDamageFormula] physical damage instead.

", + "description": "

When the @Lookup[@name] have marked half or more of their HP, their standard attack deals 1d4+2 physical damage instead.

", "resource": null, "actions": {}, "originItemType": null, diff --git a/tools/analyze-damage.mjs b/tools/analyze-damage.mjs deleted file mode 100644 index 6d5da3de..00000000 --- a/tools/analyze-damage.mjs +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Internal script to analyze damage and spit out results. - * There isn't enough entries in the database to make a full analysis, some tiers miss some types. - * This script only checks for "minions" and "everything else". - * Maybe if future book monsters can be part of what we release, we can analyze those too. - */ - -import fs from "fs/promises"; -import path from "path"; - -const allData = []; - -// Read adversary pack data for average damage for attacks -const adversariesDirectory = path.join("src/packs/adversaries"); -for (const basefile of await fs.readdir(adversariesDirectory)) { - if (!basefile.endsWith(".json")) continue; - const filepath = path.join(adversariesDirectory, basefile); - const data = JSON.parse(await fs.readFile(filepath, "utf8")); - if (data?.type !== "adversary" || data.system.type === "social") continue; - - allData.push({ - name: data.name, - tier: data.system.tier, - adversaryType: data.system.type, - damage: parseDamage(data.system.attack.damage), - }); -} - -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")), -}; - -console.log(result); - -/** Compiles all data for an adversary type (or all entries) */ -function compileData(entries) { - // Note: sorting numbers sorts by their string version by default - 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); - results[tier] = { - 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') - .map(p => - p.value.custom.enabled - ? p.value.custom.formula - : [p.value.flatMultiplier ? `${p.value.flatMultiplier}${p.value.dice}` : 0, p.value.bonus ?? 0] - .filter(p => !!p) - .join('+') - ) - .join('+'); - return getExpectedDamage(formula); -} - -/** - * Given a simple flavor-less formula with only +/- operators, returns a list of damage partial terms. - * All subtracted terms become negative terms. - */ -function getExpectedDamage(formula) { - const terms = formula.replace("+", " + ").replace("-", " - ").split(" ").map(t => t.trim()); - let multiplier = 1; - return terms.reduce((total, term) => { - if (term === "-") { - multiplier = -1; - return total; - } else if (term === "+") { - return total; - } - - const currentMultiplier = multiplier; - multiplier = 1; - - const number = Number(term); - if (!Number.isNaN(number)) { - return total + currentMultiplier * number; - } - - const dieMatch = term.match(/(\d+)d(\d+)/); - if (dieMatch) { - const numDice = Number(dieMatch[1]); - const faces = Number(dieMatch[2]); - return total + currentMultiplier * numDice * ((faces + 1) / 2); - } - - throw Error(`Unexpected term ${term} in formula ${formula}`); - }, 0); -}