From ce47c63ce6270bc10a6573ca2a8eaf221e6b348d Mon Sep 17 00:00:00 2001 From: WBHarry Date: Sat, 18 Jul 2026 00:30:14 +0200 Subject: [PATCH 01/10] Corrected ChatDamageData and made damage.main nullable --- module/data/action/baseAction.mjs | 3 ++ module/data/chat-message/actorRoll.mjs | 33 +++++++++++++-------- module/data/chat-message/chatDamageData.mjs | 8 +++-- module/data/fields/action/damageField.mjs | 2 +- 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index 944d373b..04a93d7a 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -471,6 +471,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel } if (source.damage && source.damage.resources === undefined) { + source.damage.main = null; source.damage.resources = {}; for (const [partKey, part] of Object.entries(source.damage.parts)) { if (partKey === 'hitPoints') { @@ -484,6 +485,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel source.damage.resources[partKey] = part; } } + + delete source.damage.parts; } } } diff --git a/module/data/chat-message/actorRoll.mjs b/module/data/chat-message/actorRoll.mjs index 0b5d2678..2d578a0f 100644 --- a/module/data/chat-message/actorRoll.mjs +++ b/module/data/chat-message/actorRoll.mjs @@ -188,19 +188,28 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { } static migrateData(source) { - if (source.hasDamage && !source.damage.types) { - source.damage = { - types: Object.keys(source.damage).reduce((acc, key) => { - const damageData = source.damage[key]; - const oldRoll = damageData.parts[0]?.roll; - acc[key] = oldRoll ? { - ...oldRoll, - options: { - ...oldRoll.options, - damageTypes: damageData.parts[0].damageTypes ?? [] - } - } : null; + if (source.hasDamage && !source.damage.resources === undefined) { + const getRoll = key => { + const damageData = source.damage[key]; + const oldRoll = damageData.parts[0]?.roll; + return oldRoll ? { + ...oldRoll, + options: { + ...oldRoll.options, + damageTypes: damageData.parts[0].damageTypes ?? [] + } + } : null; + }; + source.damage = { + main: source.damage.hitPoints ? getRoll('hitPoints') : null, + resources: Object.keys(source.damage).reduce((acc, key) => { + if (key === 'hitPoints') return acc; + + const roll = getRoll(key); + if (!roll) return acc; + + acc[key] = roll; return acc; }, {}) }; diff --git a/module/data/chat-message/chatDamageData.mjs b/module/data/chat-message/chatDamageData.mjs index 0de9ea9e..bf61c892 100644 --- a/module/data/chat-message/chatDamageData.mjs +++ b/module/data/chat-message/chatDamageData.mjs @@ -11,7 +11,7 @@ export class ChatDamageData extends foundry.abstract.DataModel { const fields = foundry.data.fields; return { - damage: new fields.JSONField({validate: ChatDamageData.#validateRoll}), + main: new fields.JSONField({ nullable: true, validate: ChatDamageData.#validateRoll}), resources: new fields.TypedObjectField(new fields.JSONField({validate: ChatDamageData.#validateRoll})) }; } @@ -21,8 +21,10 @@ export class ChatDamageData extends foundry.abstract.DataModel { } static #validateRoll(rollJSON) { - const roll = JSON.parse(rollJSON); - if (!roll.evaluated) throw new Error('Roll objects added to ChatMessage documents must be evaluated'); + if (rollJSON) { + const roll = JSON.parse(rollJSON); + if (!roll.evaluated) throw new Error('Roll objects added to ChatMessage documents must be evaluated'); + } } _prepareRolls() { diff --git a/module/data/fields/action/damageField.mjs b/module/data/fields/action/damageField.mjs index 72933c60..87f50e4b 100644 --- a/module/data/fields/action/damageField.mjs +++ b/module/data/fields/action/damageField.mjs @@ -13,7 +13,7 @@ export default class DamageField extends fields.SchemaField { /** @inheritDoc */ constructor(options, context = {}) { const damageFields = { - main: new fields.EmbeddedDataField(DHDamageData), + main: new fields.EmbeddedDataField(DHDamageData, { nullable: true }), resources: new IterableTypedObjectField(DHResourceData) }; super(damageFields, options, context); From 1f12a98c6379d3a88adca4db699b9bc9a1ca69b6 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Sat, 18 Jul 2026 00:49:56 +0200 Subject: [PATCH 02/10] Sheet fixes --- module/data/action/attackAction.mjs | 2 +- module/data/actor/adversary.mjs | 12 +++++------- module/data/actor/character.mjs | 16 +++++++--------- module/data/item/weapon.mjs | 16 +++++++--------- 4 files changed, 20 insertions(+), 26 deletions(-) diff --git a/module/data/action/attackAction.mjs b/module/data/action/attackAction.mjs index 1988b1d8..ed89f18c 100644 --- a/module/data/action/attackAction.mjs +++ b/module/data/action/attackAction.mjs @@ -73,7 +73,7 @@ export default class DHAttackAction extends DHDamageAction { if (range) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.short`)); const useAltDamage = this.actor?.effects?.find(x => x.type === 'horde')?.active; - for (const { value, valueAlt, type } of damage.parts) { + for (const { value, valueAlt, type } of [damage.main, ...damage.resources]) { const usedValue = useAltDamage ? valueAlt : value; const damageString = Roll.replaceFormulaData(usedValue.getFormula(), this.actor?.getRollData() ?? {}); const str = damageString diff --git a/module/data/actor/adversary.mjs b/module/data/actor/adversary.mjs index ae17c128..2640211f 100644 --- a/module/data/actor/adversary.mjs +++ b/module/data/actor/adversary.mjs @@ -84,13 +84,11 @@ export default class DhpAdversary extends DhCreature { type: 'attack' }, damage: { - parts: { - hitPoints: { - type: ['physical'], - applyTo: 'hitPoints', - value: { - multiplier: 'flat' - } + main: { + type: ['physical'], + applyTo: 'hitPoints', + value: { + multiplier: 'flat' } } } diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 53581e79..8ff50a5d 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -104,15 +104,13 @@ export default class DhCharacter extends DhCreature { trait: 'strength' }, damage: { - parts: { - hitPoints: { - type: ['physical'], - applyTo: 'hitPoints', - value: { - custom: { - enabled: true, - formula: '@profd4' - } + main: { + type: ['physical'], + applyTo: 'hitPoints', + value: { + custom: { + enabled: true, + formula: '@profd4' } } } diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 42326f93..dd503989 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -67,13 +67,11 @@ export default class DHWeapon extends AttachableItem { type: 'attack' }, damage: { - parts: { - hitPoints: { - type: ['physical'], - value: { - multiplier: 'prof', - dice: 'd8' - } + main: { + type: ['physical'], + value: { + multiplier: 'prof', + dice: 'd8' } } } @@ -230,7 +228,7 @@ export default class DHWeapon extends AttachableItem { game.i18n.localize(`DAGGERHEART.CONFIG.Burden.${burden}`) ]; - for (const { value, type } of attack.damage.parts) { + for (const { value, type } of [attack.damage.main, ...attack.damage.resources]) { const parts = value.custom.enabled ? [game.i18n.localize('DAGGERHEART.GENERAL.custom')] : [value.dice]; if (!value.custom.enabled && value.bonus) parts.push(value.bonus.signedString()); @@ -258,7 +256,7 @@ export default class DHWeapon extends AttachableItem { if (roll.trait) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${roll.trait}.short`)); if (range) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.short`)); - for (const { value, type } of damage.parts) { + for (const { value, type } of [damage.main, ...damage.resources]) { const str = Roll.replaceFormulaData(value.getFormula(), this.actor?.getRollData() ?? {}); const icons = Array.from(type) From ed30fd21225841cd067f1ad67c41415cbd92c581 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Fri, 17 Jul 2026 20:52:00 -0400 Subject: [PATCH 03/10] Fix most issues with damage effects tab --- .../sheets-configs/action-base-config.mjs | 69 ++++++-- module/data/fields/action/damageField.mjs | 5 +- module/helpers/utils.mjs | 13 -- templates/actionTypes/damage.hbs | 150 ++++++++++-------- .../action-settings/effect.hbs | 2 +- 5 files changed, 140 insertions(+), 99 deletions(-) diff --git a/module/applications/sheets-configs/action-base-config.mjs b/module/applications/sheets-configs/action-base-config.mjs index b65e1cdf..46e75ba7 100644 --- a/module/applications/sheets-configs/action-base-config.mjs +++ b/module/applications/sheets-configs/action-base-config.mjs @@ -1,4 +1,4 @@ -import { getUnusedDamageTypes } from '../../helpers/utils.mjs'; +import { DHDamageData } from '../../data/fields/action/damageField.mjs'; import DaggerheartSheet from '../sheets/daggerheart-sheet.mjs'; const { ApplicationV2 } = foundry.applications.api; @@ -31,8 +31,10 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) removeElement: this.removeElement, removeTransformActor: this.removeTransformActor, editEffect: this.editEffect, - addDamage: this.addDamage, - removeDamage: this.removeDamage, + addDamage: this.#onAddDamage, + removeDamage: this.#onRemoveDamage, + addDamageResource: this.#onAddDamageResource, + removeDamageResource: this.#onRemoveDamageResource, editDoc: this.editDoc, addTrigger: this.addTrigger, removeTrigger: this.removeTrigger, @@ -157,9 +159,8 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) context.tabs = this._getTabs(this.constructor.TABS); context.config = CONFIG.DH; if (this.action.damage) { - context.allDamageTypesUsed = !getUnusedDamageTypes(this.action.damage.parts).length; - - if (this.action.damage.hasOwnProperty('includeBase') && this.action.type === 'attack') + context.allDamageTypesUsed = !this.#getUnusedDamageTypes().length; + if (this.action.damage?.main?.hasOwnProperty('includeBase') && this.action.type === 'attack') context.hasBaseDamage = !!this.action.parent.attack; } @@ -231,6 +232,23 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) return filtered; } + /** + * Gets unused resource types of the damage field + * @returns {{ value: string; label: string }[]} + */ + #getUnusedDamageTypes() { + const usedKeys = Object.keys(this.action._source.damage.resources); + return Object.keys(CONFIG.DH.GENERAL.healingTypes).reduce((acc, key) => { + if (!usedKeys.includes(key)) + acc.push({ + value: key, + label: game.i18n.localize(CONFIG.DH.GENERAL.healingTypes[key].label) + }); + + return acc; + }, []); + } + _prepareSubmitData(_event, formData) { const submitData = foundry.utils.expandObject(formData.object); @@ -299,10 +317,30 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); } - static addDamage(_event) { - if (!this.action.damage.parts) return; + static #onAddDamage() { + if (!this.action.damage || this.action.damage?.main) return; - const choices = getUnusedDamageTypes(this.action._source.damage.parts); + const data = this.action.toObject(); + data.damage.main = { + ...DHDamageData.schema.getInitialValue(), + applyTo: 'hitPoints', + type: 'physical' + }; + this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); + } + + static #onRemoveDamage() { + if (!this.action.damage?.main) return; + const data = this.action.toObject(); + data.damage.main = null; + this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); + } + + /** @this DHActionBaseConfig */ + static #onAddDamageResource(_event) { + if (!this.action.damage) return; + + const choices = this.#getUnusedDamageTypes(); const content = new foundry.data.fields.StringField({ label: game.i18n.localize('Damage Type'), choices, @@ -320,12 +358,12 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) const callback = (_, button) => { const data = this.action.toObject(); const type = choices[button.form.elements.type.value].value; - const part = this.action.schema.fields.damage.fields.parts.element.getInitialValue(); + const part = this.action.schema.fields.damage.fields.resources.element.getInitialValue(); part.applyTo = type; if (type === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) - part.type = this.action.schema.fields.damage.fields.parts.element.fields.type.element.initial; + part.type = this.action.schema.fields.damage.fields.resources.element.fields.type.element.initial; - data.damage.parts[type] = part; + data.damage.resources[type] = part; this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); }; @@ -353,12 +391,11 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) typeDialog.render(true); } - static removeDamage(_event, button) { - if (!this.action.damage.parts) return; + static #onRemoveDamageResource(_event, button) { + if (!this.action.damage?.resources) return; const data = this.action.toObject(); const key = button.dataset.key; - delete data.damage.parts[key]; - data.damage.parts[`${key}`] = _del; + data.damage.resources[key] = _del; this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); } diff --git a/module/data/fields/action/damageField.mjs b/module/data/fields/action/damageField.mjs index 87f50e4b..a425b852 100644 --- a/module/data/fields/action/damageField.mjs +++ b/module/data/fields/action/damageField.mjs @@ -12,11 +12,10 @@ export default class DamageField extends fields.SchemaField { /** @inheritDoc */ constructor(options, context = {}) { - const damageFields = { + super({ main: new fields.EmbeddedDataField(DHDamageData, { nullable: true }), resources: new IterableTypedObjectField(DHResourceData) - }; - super(damageFields, options, context); + }, options, context); } /** diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 84bcacf2..f600eae6 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -700,19 +700,6 @@ export async function RefreshFeatures( return refreshedActors; } -export function getUnusedDamageTypes(parts) { - const usedKeys = Object.keys(parts); - return Object.keys(CONFIG.DH.GENERAL.healingTypes).reduce((acc, key) => { - if (!usedKeys.includes(key)) - acc.push({ - value: key, - label: game.i18n.localize(CONFIG.DH.GENERAL.healingTypes[key].label) - }); - - return acc; - }, []); -} - /** Returns resolved armor sources ordered by application order */ export function getArmorSources(actor) { const rawArmorSources = Array.from(actor.allApplicableEffects()).filter(x => x.system.armorData); diff --git a/templates/actionTypes/damage.hbs b/templates/actionTypes/damage.hbs index 03300840..995f8949 100644 --- a/templates/actionTypes/damage.hbs +++ b/templates/actionTypes/damage.hbs @@ -1,4 +1,3 @@ -
{{#if (eq @root.source.type 'healing')}} @@ -6,87 +5,106 @@ {{else}} {{localize "DAGGERHEART.GENERAL.damage"}} {{/if}} - {{#unless (eq path 'system.attack.')}}{{/unless}} + {{#if source.main}} + + {{else}} + + {{/if}} + + + {{#if source.main}} +
+ {{#if @root.hasBaseDamage}} + {{formField @root.fields.damage.fields.main.fields.includeBase value=@root.source.damage.includeBase name="damage.main.includeBase" classes="checkbox" localize=true }} + {{/if}} + {{#unless (eq @root.source.type 'healing')}} + {{formField baseFields.main.fields.direct value=source.main.direct name=(concat path "damage.main.direct") localize=true classes="checkbox"}} + {{/unless}} + {{#if (and @root.isNPC (not (eq path 'system.attack.')))}} + {{formField baseFields.main.fields.groupAttack value=source.main.groupAttack name=(concat path "damage.main.groupAttack") localize=true classes="select"}} + {{/if}} +
+ {{> damageData data=source.main fields=fields.main.fields basePath=(concat path "damage.main")}} + {{/if}} +
+ +
+ + {{localize "DAGGERHEART.GENERAL.Resource.plural"}} + {{#unless (eq path 'system.attack.')}}{{/unless}} -
- {{#if @root.hasBaseDamage}} - {{formField @root.fields.damage.fields.includeBase value=@root.source.damage.includeBase name="damage.includeBase" classes="checkbox" localize=true }} - {{/if}} - {{#unless (eq @root.source.type 'healing')}} - {{formField baseFields.direct value=source.direct name=(concat path "damage.direct") localize=true classes="checkbox"}} - {{/unless}} - {{#if (and @root.isNPC (not (eq path 'system.attack.')))}} - {{formField baseFields.groupAttack value=source.groupAttack name=(concat path "damage.groupAttack") localize=true classes="select"}} - {{/if}} -
{{!-- Handlebars uses Symbol.Iterator to produce index|key. This isn't compatible with our parts object, so we instead use applyTo, which is the same value --}} - {{#each source.parts as |dmg key|}} + {{#each source.resources as |dmg key|}}
{{localize (concat "DAGGERHEART.CONFIG.HealingType." dmg.applyTo ".name")}} {{#unless (or dmg.base ../path)}} - + {{/unless}} - - {{#if (and (not @root.isNPC) @root.hasRoll (not dmg.base))}} - {{formField ../fields.resultBased value=dmg.resultBased name=(concat "damage.parts." dmg.applyTo ".resultBased") localize=true classes="checkbox"}} - {{/if}} - {{#if (and (not @root.isNPC) @root.hasRoll (not dmg.base) dmg.resultBased)}} -
-
- {{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.hope")}} - {{> formula fields=../fields.value.fields type=../fields.type dmg=dmg source=dmg.value target="value" key=dmg.applyTo path=../path}} -
-
- {{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.fear")}} - {{> formula fields=../fields.valueAlt.fields type=../fields.type dmg=dmg source=dmg.valueAlt target="valueAlt" key=dmg.applyTo path=../path}} -
-
- {{else}} - {{> formula fields=../fields.value.fields type=../fields.type dmg=dmg source=dmg.value target="value" key=dmg.applyTo path=../path}} - {{/if}} - - {{#if (and (eq dmg.applyTo 'hitPoints') (ne @root.source.type 'healing'))}} - {{formField ../fields.type value=dmg.type name=(concat ../path "damage.parts." dmg.applyTo ".type") localize=true}} - {{/if}} - - {{#if ../horde}} -
- {{localize "DAGGERHEART.ACTORS.Adversary.hordeDamage"}} -
- - {{formField ../fields.valueAlt.fields.flatMultiplier value=dmg.valueAlt.flatMultiplier name=(concat ../path "damage.parts." dmg.applyTo ".valueAlt.flatMultiplier") label="DAGGERHEART.ACTIONS.Settings.multiplier" classes="inline-child" localize=true }} - {{formField ../fields.valueAlt.fields.dice value=dmg.valueAlt.dice name=(concat ../path "damage.parts." dmg.applyTo ".valueAlt.dice") classes="inline-child" localize=true}} - {{formField ../fields.valueAlt.fields.bonus value=dmg.valueAlt.bonus name=(concat ../path "damage.parts." dmg.applyTo ".valueAlt.bonus") localize=true classes="inline-child"}} -
-
- {{/if}} - + {{> damageData data=dmg fields=../fields.resources.element.fields basePath=(concat path "damage.resources." dmg.applyTo)}}
{{/each}} {{#*inline "formula"}} - {{#unless dmg.base}} - {{formField fields.custom.fields.enabled value=source.custom.enabled name=(concat path "damage.parts." key "." target ".custom.enabled") classes="checkbox" localize=true}} - {{/unless}} - {{#if source.custom.enabled}} - {{formField fields.custom.fields.formula value=source.custom.formula name=(concat path "damage.parts." key "." target ".custom.formula") localize=true}} - {{else}} + {{#unless dmg.base}} + {{formField fields.custom.fields.enabled value=source.custom.enabled name=(concat basePath "." target ".custom.enabled") classes="checkbox" localize=true}} + {{/unless}} + {{#if source.custom.enabled}} + {{formField fields.custom.fields.formula value=source.custom.formula name=(concat basePath "." target ".custom.formula") localize=true}} + {{else}} +
+ {{#unless @root.isNPC}} + {{formField fields.multiplier value=source.multiplier name=(concat basePath "." target ".multiplier") localize=true}} + {{/unless}} + {{#if (eq source.multiplier 'flat')}}{{formField fields.flatMultiplier value=source.flatMultiplier name=(concat basePath "." target ".flatMultiplier") localize=true }}{{/if}} + {{formField fields.dice value=source.dice name=(concat basePath "." target ".dice") localize=true}} + {{formField fields.bonus value=source.bonus name=(concat basePath "." target ".bonus") localize=true}} +
+ {{/if}} + {{#if @root.isNPC}} + + {{/if}} +{{/inline}} + +{{#*inline "damageData"}} + {{#if (and (not @root.isNPC) @root.hasRoll (not data.base))}} + {{formField fields.resultBased value=data.resultBased name=(concat basePath ".resultBased") localize=true classes="checkbox"}} + {{/if}} + {{#if (and (not @root.isNPC) @root.hasRoll (not data.base) data.resultBased)}} +
+
+ {{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.hope")}} + {{> formula fields=fields.value.fields type=fields.type dmg=data source=data.value target="value" key=data.applyTo path=../path}} +
+
+ {{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.fear")}} + {{> formula fields=fields.valueAlt.fields type=fields.type dmg=data source=data.valueAlt target="valueAlt" key=data.applyTo path=../path}} +
+
+ {{else}} + {{> formula fields=fields.value.fields type=fields.type dmg=data source=data.value basePath=basePath target="value" key=data.applyTo path=../path}} + {{/if}} + + {{#if (ne @root.source.type 'healing')}} + {{formField fields.type value=data.type name=(concat basePath ".type") localize=true}} + {{/if}} + + {{#if ../horde}} +
+ {{localize "DAGGERHEART.ACTORS.Adversary.hordeDamage"}}
- {{#unless @root.isNPC}} - {{formField fields.multiplier value=source.multiplier name=(concat path "damage.parts." key "." target ".multiplier") localize=true}} - {{/unless}} - {{#if (eq source.multiplier 'flat')}}{{formField fields.flatMultiplier value=source.flatMultiplier name=(concat path "damage.parts." key "." target ".flatMultiplier") localize=true }}{{/if}} - {{formField fields.dice value=source.dice name=(concat path "damage.parts." key "." target ".dice") localize=true}} - {{formField fields.bonus value=source.bonus name=(concat path "damage.parts." key "." target ".bonus") localize=true}} + + + {{formField fields.valueAlt.fields.flatMultiplier value=data.valueAlt.flatMultiplier name=(concat basePath ".valueAlt.flatMultiplier") label="DAGGERHEART.ACTIONS.Settings.multiplier" classes="inline-child" localize=true }} + {{formField fields.valueAlt.fields.dice value=data.valueAlt.dice name=(concat basePath ".valueAlt.dice") classes="inline-child" localize=true}} + {{formField fields.valueAlt.fields.bonus value=data.valueAlt.bonus name=(concat basePath ".valueAlt.bonus") localize=true classes="inline-child"}}
- {{/if}} - {{#if @root.isNPC}} - - {{/if}} +
+ {{/if}} + {{/inline}} \ No newline at end of file diff --git a/templates/sheets-settings/action-settings/effect.hbs b/templates/sheets-settings/action-settings/effect.hbs index 567cb81c..67017a5a 100644 --- a/templates/sheets-settings/action-settings/effect.hbs +++ b/templates/sheets-settings/action-settings/effect.hbs @@ -5,7 +5,7 @@ > {{#if fields.roll}}{{> 'systems/daggerheart/templates/actionTypes/roll.hbs' fields=fields.roll.fields source=source.roll}}{{/if}} {{#if fields.save}}{{> 'systems/daggerheart/templates/actionTypes/save.hbs' fields=fields.save.fields source=source.save}}{{/if}} - {{#if fields.damage}}{{> 'systems/daggerheart/templates/actionTypes/damage.hbs' fields=fields.damage.fields.parts.element.fields source=source.damage baseFields=fields.damage.fields }}{{/if}} + {{#if fields.damage}}{{> 'systems/daggerheart/templates/actionTypes/damage.hbs' fields=fields.damage.fields source=source.damage baseFields=fields.damage.fields }}{{/if}} {{#if fields.macro}}{{> 'systems/daggerheart/templates/actionTypes/macro.hbs' fields=fields.macro source=source.macro}}{{/if}} {{#if fields.effects}}{{> 'systems/daggerheart/templates/actionTypes/effect.hbs' fields=fields.effects.element.fields source=source.effects}}{{/if}} {{#if fields.beastform}}{{> 'systems/daggerheart/templates/actionTypes/beastform.hbs' fields=fields.beastform.fields source=source.beastform}}{{/if}} From 42ec4f8c301149d833efcb54c1391629292e51e4 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Sat, 18 Jul 2026 03:09:37 +0200 Subject: [PATCH 04/10] Fixed ChatMessage damage creation --- module/applications/dialogs/damageDialog.mjs | 9 +- module/data/action/baseAction.mjs | 4 +- module/data/action/damageAction.mjs | 7 +- module/data/chat-message/chatDamageData.mjs | 14 +- module/data/fields/action/damageField.mjs | 29 ++-- module/dice/damageRoll.mjs | 138 ++++++++++-------- module/dice/dhRoll.mjs | 6 +- .../dialogs/dice-roll/damageSelection.hbs | 58 ++++---- 8 files changed, 152 insertions(+), 113 deletions(-) diff --git a/module/applications/dialogs/damageDialog.mjs b/module/applications/dialogs/damageDialog.mjs index 46d3d41f..8b37cf2d 100644 --- a/module/applications/dialogs/damageDialog.mjs +++ b/module/applications/dialogs/damageDialog.mjs @@ -51,7 +51,11 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application const context = await super._prepareContext(_options); context.config = CONFIG.DH; context.title = this.config.title ?? this.title; - context.formula = this.roll.constructFormula(this.config); + + const { damageFormula, resourceFormulas } = this.roll.constructFormulas(this.config); + context.damageFormula = damageFormula; + context.resourceFormulas = resourceFormulas; + context.hasHealing = this.config.hasHealing; context.directDamage = this.config.directDamage; context.selectedMessageMode = this.config.selectedMessageMode; @@ -73,7 +77,8 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application static updateRollConfiguration(_event, _, formData) { const data = foundry.utils.expandObject(formData.object); - foundry.utils.mergeObject(this.config.roll, data.roll); + foundry.utils.mergeObject(this.config.damageFormula, data.damageFormula); + foundry.utils.mergeObject(this.config.resourceFormulas, data.resourceFormulas); foundry.utils.mergeObject(this.config.modifiers, data.modifiers); this.config.selectedMessageMode = data.selectedMessageMode; diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index 04a93d7a..1e914e6f 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -429,11 +429,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel } get hasDamage() { - return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type !== 'healing'; + return this.type !== 'healing' && Boolean(this.damage.main) || Boolean(this.damage.resources.length); } get hasHealing() { - return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type === 'healing'; + return this.type === 'healing' && Boolean(this.damage.main) || Boolean(this.damage.resources.length); } get hasSave() { diff --git a/module/data/action/damageAction.mjs b/module/data/action/damageAction.mjs index 51735543..15135e0d 100644 --- a/module/data/action/damageAction.mjs +++ b/module/data/action/damageAction.mjs @@ -8,11 +8,8 @@ export default class DHDamageAction extends DHBaseAction { * @returns Formula string */ getDamageFormula() { - const strings = []; - for (const { value } of this.damage.parts) { - strings.push(Roll.replaceFormulaData(value.getFormula(), this.actor?.getRollData() ?? {})); - } + if (!this.damage.main) return ''; - return strings.join(' + '); + return Roll.replaceFormulaData(this.damage.main.value.getFormula(), this.actor?.getRollData() ?? {}); } } diff --git a/module/data/chat-message/chatDamageData.mjs b/module/data/chat-message/chatDamageData.mjs index bf61c892..c612729e 100644 --- a/module/data/chat-message/chatDamageData.mjs +++ b/module/data/chat-message/chatDamageData.mjs @@ -17,7 +17,7 @@ export class ChatDamageData extends foundry.abstract.DataModel { } get active() { - return Boolean(Object.keys(this.types).length); + return !!this.main || Boolean(Object.keys(this.resources).length); } static #validateRoll(rollJSON) { @@ -28,12 +28,12 @@ export class ChatDamageData extends foundry.abstract.DataModel { } _prepareRolls() { - for (const key of Object.keys(this.types)) { - const type = this.types[key]; - try { - this.types[key] = Roll.fromData(type); - this.types[key].options.modifierTotal = CONFIG.Dice.daggerheart.DHRoll.calculateTotalModifiers(type); - } catch {} + if (this.main) { + this.main = Roll.fromData(this.main); + } + + for (const key of Object.keys(this.resources)) { + this.resources[key] = Roll.fromData(this.resources[key]); } } diff --git a/module/data/fields/action/damageField.mjs b/module/data/fields/action/damageField.mjs index 87f50e4b..58c33997 100644 --- a/module/data/fields/action/damageField.mjs +++ b/module/data/fields/action/damageField.mjs @@ -32,25 +32,23 @@ export default class DamageField extends fields.SchemaField { this.hasRoll && DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.never.id && !force - ) + ) { return; + } - let formulas = this.damage.parts.map(p => ({ - formula: DamageField.getFormulaValue.call(this, p, config).getFormula(this.actor), - damageTypes: p.applyTo === 'hitPoints' && !p.type.size ? new Set(['physical']) : p.type, - applyTo: p.applyTo - })); + const damageFormula = this.damage.main ? + DamageField.formatFormulas.call(this, [this.damage.main], config)[0] : null; + const resourceFormulas = DamageField.formatFormulas.call(this, this.damage.resources, config); - if (!formulas.length) return false; - - formulas = DamageField.formatFormulas.call(this, formulas, config); + if (!damageFormula && !resourceFormulas.length) return false; messageId = config.message?._id ?? messageId; const message = game.messages.get(messageId); const damageConfig = { dialog: {}, ...config, - roll: formulas, + damageFormula, + resourceFormulas, data: this.getRollData(), isCritical: Boolean(message?.system.roll?.isCritical) }; @@ -175,11 +173,17 @@ export default class DamageField extends fields.SchemaField { /** * Prepare formulas for Damage Roll * Must be called within Action context or similar. - * @param {object[]} formulas Array of formatted formulas object + * @param {DHResourceData[]} damageData Array of DHResourceData * @param {object} data Action getRollData * @returns */ - static formatFormulas(formulas, data) { + static formatFormulas(damageData, data) { + const formulas = damageData.map(x => ({ + formula: DamageField.getFormulaValue.call(this, x, data).getFormula(this.actor), + damageTypes: x.applyTo === 'hitPoints' && !x.type.size ? new Set(['physical']) : x.type, + applyTo: x.applyTo + })); + const formattedFormulas = []; formulas.forEach(formula => { if (isNaN(formula.formula)) @@ -190,6 +194,7 @@ export default class DamageField extends fields.SchemaField { if (same) same.formula += ` + ${formula.formula}`; else formattedFormulas.push(formula); }); + return formattedFormulas; } diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index 8667d89f..64077e1f 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -13,16 +13,27 @@ export default class DamageRoll extends DHRoll { static DefaultDialog = DamageDialog; + static createRollInstance(config) { + return new this(undefined, config.data, config); + } + /** @inheritdoc */ static async buildEvaluate(roll, config = {}) { - if (config.dialog.configure === false) roll.constructFormula(config); + if (config.dialog.configure === false) roll.constructFormulas(config); - for (const roll of config.roll) { + const evaluateRoll = async roll => { await roll.roll.evaluate(); roll.roll.options = { damageTypes: roll.damageTypes ? [...roll.damageTypes] : [] }; + return roll.roll; + } - if (!config.damage?.types) config.damage = { types: {} }; - config.damage.types[roll.applyTo] = roll.roll; + config.damage.main = await evaluateRoll(config.damageFormula); + config.damage.main.options = { damageTypes: + config.damageFormula.damageTypes ? [...config.damageFormula.damageTypes] : [] + }; + + for (const roll of config.resourceFormulas) { + config.damage.resources[roll.applyTo] = await evaluateRoll(roll); } roll._evaluated = true; @@ -51,7 +62,8 @@ export default class DamageRoll extends DHRoll { if (config.source?.message) { chatMessage.update({ 'system.damage': { ...config.damage.toObject(), - types: config.damage.types + main: config.damage.main, + resources: config.damage.resources }}); } } @@ -125,62 +137,72 @@ export default class DamageRoll extends DHRoll { return changeKeys; } - constructFormula(config) { + constructFormulas(config) { + return { + damageFormula: this.constructFormula(this.options.damageFormula, config, true), + resourceFormulas: this.options.resourceFormulas.map(x => this.constructFormula(x, config)) + }; + } + + constructFormula(formulaData, config, isDamage) { this.options.isCritical = config.isCritical; - for (const [index, part] of this.options.roll.entries()) { - const isHitpointPart = part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id; - part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data)); - part.roll.terms = Roll.parse(part.roll.formula, config.data); - if (part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { - part.modifiers = this.applyBaseBonus(part); - this.addModifiers(part); - part.modifiers?.forEach(m => { - part.roll.terms.push(...this.formatModifier(m.value)); - }); - } - /* To Remove When Reaction System */ - if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { - for (const mod in config.modifiers) { - const modifier = config.modifiers[mod]; - if (modifier.beforeCrit === true && (modifier.enabled || modifier.value)) modifier.callback(part); - } - } - - if (part.extraFormula) { - part.roll.terms.push( - new foundry.dice.terms.OperatorTerm({ operator: '+' }), - ...this.constructor.parse(part.extraFormula, this.options.data) - ); - } - - if (config.damageOptions.groupAttack?.numAttackers > 1 && isHitpointPart) { - const damageTypes = [foundry.dice.terms.Die, foundry.dice.terms.NumericTerm]; - for (const term of part.roll.terms) { - if (damageTypes.some(type => term instanceof type)) { - term.number *= config.damageOptions.groupAttack.numAttackers; - } - } - } - - if (config.isCritical && isHitpointPart) { - const total = part.roll.dice.reduce((acc, term) => acc + term._faces * term._number, 0); - if (total > 0) { - part.roll.terms.push(...this.formatModifier(total)); - } - } - - /* To Remove When Reaction System */ - if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { - for (const mod in config.modifiers) { - const modifier = config.modifiers[mod]; - if (!modifier.beforeCrit && (modifier.enabled || modifier.value)) modifier.callback(part); - } - } - - part.roll._formula = this.constructor.getFormula(part.roll.terms); + const isHitpointPart = formulaData.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id; + formulaData.roll = new Roll(Roll.replaceFormulaData(formulaData.formula, config.data)); + formulaData.roll.terms = Roll.parse(formulaData.roll.formula, config.data); + if (formulaData.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { + formulaData.modifiers = this.applyBaseBonus(formulaData); + this.addModifiers(formulaData); + formulaData.modifiers?.forEach(m => { + formulaData.roll.terms.push(...this.formatModifier(m.value)); + }); } - return this.options.roll; + + /* To Remove When Reaction System */ + if (isDamage && formulaData.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { + for (const mod in config.modifiers) { + const modifier = config.modifiers[mod]; + if ( + modifier.beforeCrit === true && + (modifier.enabled || modifier.value) + ) modifier.callback(formulaData); + } + } + + if (formulaData.extraFormula) { + formulaData.roll.terms.push( + new foundry.dice.terms.OperatorTerm({ operator: '+' }), + ...this.constructor.parse(formulaData.extraFormula, this.options.data) + ); + } + + if (config.damageOptions.groupAttack?.numAttackers > 1 && isHitpointPart) { + const damageTypes = [foundry.dice.terms.Die, foundry.dice.terms.NumericTerm]; + for (const term of formulaData.roll.terms) { + if (damageTypes.some(type => term instanceof type)) { + term.number *= config.damageOptions.groupAttack.numAttackers; + } + } + } + + if (config.isCritical && isHitpointPart) { + const total = formulaData.roll.dice.reduce((acc, term) => acc + term._faces * term._number, 0); + if (total > 0) { + formulaData.roll.terms.push(...this.formatModifier(total)); + } + } + + /* To Remove When Reaction System */ + if (isDamage && formulaData.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { + for (const mod in config.modifiers) { + const modifier = config.modifiers[mod]; + if (!modifier.beforeCrit && (modifier.enabled || modifier.value)) modifier.callback(formulaData); + } + } + + formulaData.roll._formula = this.constructor.getFormula(formulaData.roll.terms); + + return formulaData; } /* To Remove When Reaction System */ diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index 13408329..c78caa4f 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -41,6 +41,10 @@ export default class DHRoll extends BaseRoll { return config; } + static createRollInstance(config) { + return new this(config.roll.formula, config.data, config); + } + /** * @param {Partial} config * @returns {Promise} @@ -58,7 +62,7 @@ export default class DHRoll extends BaseRoll { this.temporaryModifierBuilder(config); - let roll = new this(config.roll.formula, config.data, config); + let roll = this.createRollInstance(config); if (config.dialog.configure !== false) { // Open Roll Dialog const DialogClass = config.dialog?.class ?? this.DefaultDialog; diff --git a/templates/dialogs/dice-roll/damageSelection.hbs b/templates/dialogs/dice-roll/damageSelection.hbs index 7bcd7063..cc12e26f 100644 --- a/templates/dialogs/dice-roll/damageSelection.hbs +++ b/templates/dialogs/dice-roll/damageSelection.hbs @@ -16,31 +16,10 @@ {{/if}} - {{#each @root.formula}} -
- {{localize "DAGGERHEART.GENERAL.formula"}}: {{roll.formula}} - - {{#with (lookup @root.config.GENERAL.healingTypes applyTo)}} - {{localize label}} - {{/with}} - {{#unless @root.hasHealing}} - {{#if damageTypes}} - {{#each damageTypes as | type | }} - {{#with (lookup @root.config.GENERAL.damageTypes type)}} - - {{/with}} - {{/each}} - {{/if}} - {{/unless}} - -
-
- - -
+ {{> formula @root.damageFormula path="damageFormula"}} + + {{#each @root.resourceFormulas}} + {{> formula path=(concat "resourceFormulas." @key)}} {{/each}} {{#if damageOptions.groupAttack}} @@ -87,4 +66,31 @@ {{localize "DAGGERHEART.GENERAL.roll"}} - \ No newline at end of file + + +{{#*inline "formula"}} +
+ {{localize "DAGGERHEART.GENERAL.formula"}}: {{roll.formula}} + + {{#with (lookup @root.config.GENERAL.healingTypes applyTo)}} + {{localize label}} + {{/with}} + {{#unless @root.hasHealing}} + {{#if damageTypes}} + {{#each damageTypes as | type | }} + {{#with (lookup @root.config.GENERAL.damageTypes type)}} + + {{/with}} + {{/each}} + {{/if}} + {{/unless}} + +
+
+ + +
+{{/inline}} \ No newline at end of file From c5a64c2def91694bbc4a51cb9458a04388a69af6 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Fri, 17 Jul 2026 21:12:35 -0400 Subject: [PATCH 05/10] Move type to main damage only --- module/data/fields/action/damageField.mjs | 24 +++++++++++------------ templates/actionTypes/damage.hbs | 9 +++------ 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/module/data/fields/action/damageField.mjs b/module/data/fields/action/damageField.mjs index 7703d2a5..4edc2533 100644 --- a/module/data/fields/action/damageField.mjs +++ b/module/data/fields/action/damageField.mjs @@ -283,17 +283,6 @@ export class DHResourceData extends foundry.abstract.DataModel { static defineSchema() { return { base: new fields.BooleanField({ initial: false, readonly: true, label: 'Base' }), - type: new fields.SetField( - new fields.StringField({ - choices: CONFIG.DH.GENERAL.damageTypes, - initial: 'physical', - nullable: false, - required: true - }), - { - label: game.i18n.localize('DAGGERHEART.GENERAL.type') - } - ), applyTo: new fields.StringField({ choices: CONFIG.DH.GENERAL.healingTypes, required: true, @@ -325,7 +314,18 @@ export class DHDamageData extends DHResourceData { choices: CONFIG.DH.GENERAL.groupAttackRange, blank: true, label: 'DAGGERHEART.ACTIONS.Settings.groupAttack.label' - }) + }), + type: new fields.SetField( + new fields.StringField({ + choices: CONFIG.DH.GENERAL.damageTypes, + initial: 'physical', + nullable: false, + required: true + }), + { + label: game.i18n.localize('DAGGERHEART.GENERAL.type') + } + ) }; } } diff --git a/templates/actionTypes/damage.hbs b/templates/actionTypes/damage.hbs index 995f8949..88786850 100644 --- a/templates/actionTypes/damage.hbs +++ b/templates/actionTypes/damage.hbs @@ -25,6 +25,9 @@ {{/if}} {{> damageData data=source.main fields=fields.main.fields basePath=(concat path "damage.main")}} + {{#if (ne @root.source.type 'healing')}} + {{formField fields.main.fields.type value=source.main.type name=(concat path "damage.main.type") localize=true}} + {{/if}} {{/if}} @@ -33,8 +36,6 @@ {{localize "DAGGERHEART.GENERAL.Resource.plural"}} {{#unless (eq path 'system.attack.')}}{{/unless}} - - {{!-- Handlebars uses Symbol.Iterator to produce index|key. This isn't compatible with our parts object, so we instead use applyTo, which is the same value --}} {{#each source.resources as |dmg key|}}
@@ -90,10 +91,6 @@ {{> formula fields=fields.value.fields type=fields.type dmg=data source=data.value basePath=basePath target="value" key=data.applyTo path=../path}} {{/if}} - {{#if (ne @root.source.type 'healing')}} - {{formField fields.type value=data.type name=(concat basePath ".type") localize=true}} - {{/if}} - {{#if ../horde}}
{{localize "DAGGERHEART.ACTORS.Adversary.hordeDamage"}} From b833be4f1fc98c15a809d0a2d624c06e387ad6cf Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Fri, 17 Jul 2026 21:19:38 -0400 Subject: [PATCH 06/10] Fix tags fix --- module/data/action/attackAction.mjs | 2 +- module/data/item/weapon.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/module/data/action/attackAction.mjs b/module/data/action/attackAction.mjs index ed89f18c..a931f040 100644 --- a/module/data/action/attackAction.mjs +++ b/module/data/action/attackAction.mjs @@ -73,7 +73,7 @@ export default class DHAttackAction extends DHDamageAction { if (range) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.short`)); const useAltDamage = this.actor?.effects?.find(x => x.type === 'horde')?.active; - for (const { value, valueAlt, type } of [damage.main, ...damage.resources]) { + for (const { value, valueAlt, type } of [damage.main, ...damage.resources].filter(d => !!d)) { const usedValue = useAltDamage ? valueAlt : value; const damageString = Roll.replaceFormulaData(usedValue.getFormula(), this.actor?.getRollData() ?? {}); const str = damageString diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index dd503989..4a272b76 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -256,7 +256,7 @@ export default class DHWeapon extends AttachableItem { if (roll.trait) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${roll.trait}.short`)); if (range) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.short`)); - for (const { value, type } of [damage.main, ...damage.resources]) { + for (const { value, type } of [damage.main, ...damage.resources].filter(d => !!d)) { const str = Roll.replaceFormulaData(value.getFormula(), this.actor?.getRollData() ?? {}); const icons = Array.from(type) From 6ae07bd3a7231e683a015bc40fff3dfa02b2bac4 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Fri, 17 Jul 2026 21:33:40 -0400 Subject: [PATCH 07/10] Fix newly created adversaries lacking damage --- module/data/action/baseAction.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index 1e914e6f..12d2f26e 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -470,7 +470,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel }, {}); } - if (source.damage && source.damage.resources === undefined) { + if (source.damage?.parts && !source.damage.resources && !source.damage.main) { source.damage.main = null; source.damage.resources = {}; for (const [partKey, part] of Object.entries(source.damage.parts)) { From 23ebe6f6b52999a357a5005fbf8740078698f58c Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Fri, 17 Jul 2026 23:55:05 -0400 Subject: [PATCH 08/10] Fix adversary damage --- .../sheets-configs/action-base-config.mjs | 73 ++++++-------- .../sheets-configs/adversary-settings.mjs | 35 ++++++- module/data/action/attackAction.mjs | 2 +- module/data/item/weapon.mjs | 4 +- templates/actionTypes/damage.hbs | 97 ++++++++++--------- .../adversary-settings/attack.hbs | 2 +- 6 files changed, 114 insertions(+), 99 deletions(-) diff --git a/module/applications/sheets-configs/action-base-config.mjs b/module/applications/sheets-configs/action-base-config.mjs index 46e75ba7..920cb6d0 100644 --- a/module/applications/sheets-configs/action-base-config.mjs +++ b/module/applications/sheets-configs/action-base-config.mjs @@ -159,7 +159,8 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) context.tabs = this._getTabs(this.constructor.TABS); context.config = CONFIG.DH; if (this.action.damage) { - context.allDamageTypesUsed = !this.#getUnusedDamageTypes().length; + const allKeys = Object.keys(CONFIG.DH.GENERAL.healingTypes); + context.allDamageTypesUsed = allKeys.every(k => k in this.action._source.damage.resources); if (this.action.damage?.main?.hasOwnProperty('includeBase') && this.action.type === 'attack') context.hasBaseDamage = !!this.action.parent.attack; } @@ -232,23 +233,6 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) return filtered; } - /** - * Gets unused resource types of the damage field - * @returns {{ value: string; label: string }[]} - */ - #getUnusedDamageTypes() { - const usedKeys = Object.keys(this.action._source.damage.resources); - return Object.keys(CONFIG.DH.GENERAL.healingTypes).reduce((acc, key) => { - if (!usedKeys.includes(key)) - acc.push({ - value: key, - label: game.i18n.localize(CONFIG.DH.GENERAL.healingTypes[key].label) - }); - - return acc; - }, []); - } - _prepareSubmitData(_event, formData) { const submitData = foundry.utils.expandObject(formData.object); @@ -317,6 +301,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); } + /** @this DHActionBaseConfig */ static #onAddDamage() { if (!this.action.damage || this.action.damage?.main) return; @@ -329,6 +314,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); } + /** @this DHActionBaseConfig */ static #onRemoveDamage() { if (!this.action.damage?.main) return; const data = this.action.toObject(); @@ -340,50 +326,46 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) static #onAddDamageResource(_event) { if (!this.action.damage) return; - const choices = this.#getUnusedDamageTypes(); + const allKeys = Object.keys(CONFIG.DH.GENERAL.healingTypes); + const unused = allKeys.filter(k => !(k in this.action._source.damage.resources)); + const choices = unused.map(k => ({ value: k, label: _loc(CONFIG.DH.GENERAL.healingTypes[k].label) })); const content = new foundry.data.fields.StringField({ - label: game.i18n.localize('Damage Type'), + label: _loc('DAGGERHEART.GENERAL.damageType'), choices, required: true - }).toFormGroup( - {}, - { - name: 'type', - localize: true, - nameAttr: 'value', - labelAttr: 'label' - } - ).outerHTML; + }).toFormGroup({}, { + name: 'type', + localize: true, + nameAttr: 'value', + labelAttr: 'label' + }).outerHTML; const callback = (_, button) => { const data = this.action.toObject(); const type = choices[button.form.elements.type.value].value; - const part = this.action.schema.fields.damage.fields.resources.element.getInitialValue(); - part.applyTo = type; - if (type === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) - part.type = this.action.schema.fields.damage.fields.resources.element.fields.type.element.initial; - - data.damage.resources[type] = part; + data.damage.resources[type] = { + ...this.action.schema.fields.damage.fields.resources.element.getInitialValue(), + applyTo: type + }; this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); }; const typeDialog = new foundry.applications.api.DialogV2({ buttons: [ - foundry.utils.mergeObject( - { - action: 'ok', - label: 'Confirm', - icon: 'fas fa-check', - default: true - }, - { callback: callback } - ) + { + action: 'ok', + label: 'Confirm', + icon: 'fas fa-check', + default: true, + callback + } ], content: content, rejectClose: false, modal: false, window: { - title: game.i18n.localize('Add Damage') + /** @todo localize */ + title: 'Add Damage' }, position: { width: 300 } }); @@ -391,6 +373,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) typeDialog.render(true); } + /** @this DHActionBaseConfig */ static #onRemoveDamageResource(_event, button) { if (!this.action.damage?.resources) return; const data = this.action.toObject(); diff --git a/module/applications/sheets-configs/adversary-settings.mjs b/module/applications/sheets-configs/adversary-settings.mjs index ff3f3039..c5f036ed 100644 --- a/module/applications/sheets-configs/adversary-settings.mjs +++ b/module/applications/sheets-configs/adversary-settings.mjs @@ -1,3 +1,4 @@ +import { DHDamageData } from '../../data/fields/action/damageField.mjs'; import DHBaseActorSettings from '../sheets/api/actor-setting.mjs'; /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ @@ -8,8 +9,10 @@ export default class DHAdversarySettings extends DHBaseActorSettings { classes: ['adversary-settings'], position: { width: 455, height: 'auto' }, actions: { - addExperience: DHAdversarySettings.#addExperience, - removeExperience: DHAdversarySettings.#removeExperience + addExperience: DHAdversarySettings.#onAddExperience, + removeExperience: DHAdversarySettings.#onRemoveExperience, + addDamage: this.#onAddDamage, + removeDamage: this.#onRemoveDamage } }; @@ -71,7 +74,7 @@ export default class DHAdversarySettings extends DHBaseActorSettings { * Adds a new experience entry to the actor. * @type {ApplicationClickAction} */ - static async #addExperience() { + static async #onAddExperience() { const newExperience = { name: 'Experience', modifier: 0 @@ -83,7 +86,7 @@ export default class DHAdversarySettings extends DHBaseActorSettings { * Removes an experience entry from the actor. * @type {ApplicationClickAction} */ - static async #removeExperience(_, target) { + static async #onRemoveExperience(_, target) { const experience = this.actor.system.experiences[target.dataset.experience]; const confirmed = await foundry.applications.api.DialogV2.confirm({ window: { @@ -98,4 +101,28 @@ export default class DHAdversarySettings extends DHBaseActorSettings { await this.actor.update({ [`system.experiences.${target.dataset.experience}`]: _del }); } + + /** + * @this DHAdversarySettings + * @type {ApplicationClickAction} + */ + static #onAddDamage() { + this.actor.update({ + 'system.attack.damage.main': { + ...DHDamageData.schema.getInitialValue(), + applyTo: 'hitPoints', + type: 'physical' + } + }); + } + + /** + * @this DHAdversarySettings + * @type {ApplicationClickAction} + */ + static #onRemoveDamage() { + this.actor.update({ + 'system.attack.damage.main': null + }); + } } diff --git a/module/data/action/attackAction.mjs b/module/data/action/attackAction.mjs index a931f040..6c205de6 100644 --- a/module/data/action/attackAction.mjs +++ b/module/data/action/attackAction.mjs @@ -82,7 +82,7 @@ export default class DHAttackAction extends DHDamageAction { x: game.i18n.localize('DAGGERHEART.GENERAL.damage') }); - const icons = Array.from(type) + const icons = Array.from(type ?? []) .map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon) .filter(Boolean); diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 4a272b76..499b16c0 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -232,7 +232,7 @@ export default class DHWeapon extends AttachableItem { const parts = value.custom.enabled ? [game.i18n.localize('DAGGERHEART.GENERAL.custom')] : [value.dice]; if (!value.custom.enabled && value.bonus) parts.push(value.bonus.signedString()); - if (type.size > 0) { + if (type?.size) { const typeTags = Array.from(type) .map(t => game.i18n.localize(`DAGGERHEART.CONFIG.DamageType.${t}.abbreviation`)) .join(' | '); @@ -259,7 +259,7 @@ export default class DHWeapon extends AttachableItem { for (const { value, type } of [damage.main, ...damage.resources].filter(d => !!d)) { const str = Roll.replaceFormulaData(value.getFormula(), this.actor?.getRollData() ?? {}); - const icons = Array.from(type) + const icons = Array.from(type ?? []) .map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon) .filter(Boolean); diff --git a/templates/actionTypes/damage.hbs b/templates/actionTypes/damage.hbs index 88786850..f315f8a1 100644 --- a/templates/actionTypes/damage.hbs +++ b/templates/actionTypes/damage.hbs @@ -24,47 +24,53 @@ {{formField baseFields.main.fields.groupAttack value=source.main.groupAttack name=(concat path "damage.main.groupAttack") localize=true classes="select"}} {{/if}}
- {{> damageData data=source.main fields=fields.main.fields basePath=(concat path "damage.main")}} + {{> damageData damage=source.main fields=fields.main.fields basePath=(concat path "damage.main")}} + {{#if horde}} + {{> hordeDamage source=source.main fields=fields.main.fields basePath=(concat path "damage.main")}} + {{/if}} {{#if (ne @root.source.type 'healing')}} {{formField fields.main.fields.type value=source.main.type name=(concat path "damage.main.type") localize=true}} {{/if}} {{/if}} -
- - {{localize "DAGGERHEART.GENERAL.Resource.plural"}} - {{#unless (eq path 'system.attack.')}}{{/unless}} - - {{#each source.resources as |dmg key|}} -
- - - {{localize (concat "DAGGERHEART.CONFIG.HealingType." dmg.applyTo ".name")}} - {{#unless (or dmg.base ../path)}} - - {{/unless}} - - {{> damageData data=dmg fields=../fields.resources.element.fields basePath=(concat path "damage.resources." dmg.applyTo)}} -
- - {{/each}} - +{{#unless (eq path 'system.attack.')}} + {{! In the future, consider allowing this even on NPCs}} +
+ + {{localize "DAGGERHEART.GENERAL.Resource.plural"}} + {{#unless @root.allDamageTypesUsed}}{{/unless}} + + {{#each source.resources as |dmg key|}} +
+ + + {{localize (concat "DAGGERHEART.CONFIG.HealingType." dmg.applyTo ".name")}} + {{#unless (or dmg.base ../path)}} + + {{/unless}} + + {{> damageData damage=dmg fields=../fields.resources.element.fields basePath=(concat ../path "damage.resources." dmg.applyTo)}} +
+ + {{/each}} + +{{/unless}} {{#*inline "formula"}} - {{#unless dmg.base}} - {{formField fields.custom.fields.enabled value=source.custom.enabled name=(concat basePath "." target ".custom.enabled") classes="checkbox" localize=true}} + {{#unless isBase}} + {{formField fields.custom.fields.enabled value=source.custom.enabled name=(concat basePath ".custom.enabled") classes="checkbox" localize=true}} {{/unless}} {{#if source.custom.enabled}} - {{formField fields.custom.fields.formula value=source.custom.formula name=(concat basePath "." target ".custom.formula") localize=true}} + {{formField fields.custom.fields.formula value=source.custom.formula name=(concat basePath ".custom.formula") localize=true}} {{else}}
{{#unless @root.isNPC}} - {{formField fields.multiplier value=source.multiplier name=(concat basePath "." target ".multiplier") localize=true}} + {{formField fields.multiplier value=source.multiplier name=(concat basePath ".multiplier") localize=true}} {{/unless}} - {{#if (eq source.multiplier 'flat')}}{{formField fields.flatMultiplier value=source.flatMultiplier name=(concat basePath "." target ".flatMultiplier") localize=true }}{{/if}} - {{formField fields.dice value=source.dice name=(concat basePath "." target ".dice") localize=true}} - {{formField fields.bonus value=source.bonus name=(concat basePath "." target ".bonus") localize=true}} + {{#if (eq source.multiplier 'flat')}}{{formField fields.flatMultiplier value=source.flatMultiplier name=(concat basePath ".flatMultiplier") localize=true }}{{/if}} + {{formField fields.dice value=source.dice name=(concat basePath ".dice") localize=true}} + {{formField fields.bonus value=source.bonus name=(concat basePath ".bonus") localize=true}}
{{/if}} {{#if @root.isNPC}} @@ -73,35 +79,34 @@ {{/inline}} {{#*inline "damageData"}} - {{#if (and (not @root.isNPC) @root.hasRoll (not data.base))}} - {{formField fields.resultBased value=data.resultBased name=(concat basePath ".resultBased") localize=true classes="checkbox"}} + {{#if (and (not @root.isNPC) @root.hasRoll (not damage.base))}} + {{formField fields.resultBased value=damage.resultBased name=(concat basePath ".resultBased") localize=true classes="checkbox"}} {{/if}} - {{#if (and (not @root.isNPC) @root.hasRoll (not data.base) data.resultBased)}} + {{#if (and (not @root.isNPC) @root.hasRoll (not damage.base) damage.resultBased)}}
{{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.hope")}} - {{> formula fields=fields.value.fields type=fields.type dmg=data source=data.value target="value" key=data.applyTo path=../path}} + {{> formula key=damage.applyTo fields=fields.value.fields type=fields.type isBase=damage.base source=damage.value basePath=(concat basePath ".value")}}
{{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.fear")}} - {{> formula fields=fields.valueAlt.fields type=fields.type dmg=data source=data.valueAlt target="valueAlt" key=data.applyTo path=../path}} + {{> formula key=damage.applyTo fields=fields.valueAlt.fields type=fields.type isBase=damage.base source=damage.valueAlt basePath=(concat basePath ".valueAlt")}}
{{else}} - {{> formula fields=fields.value.fields type=fields.type dmg=data source=data.value basePath=basePath target="value" key=data.applyTo path=../path}} + {{> formula key=damage.applyTo fields=fields.value.fields type=fields.type isBase=damage.base source=damage.value basePath=(concat basePath ".value")}} {{/if}} + +{{/inline}} - {{#if ../horde}} -
- {{localize "DAGGERHEART.ACTORS.Adversary.hordeDamage"}} -
- - - {{formField fields.valueAlt.fields.flatMultiplier value=data.valueAlt.flatMultiplier name=(concat basePath ".valueAlt.flatMultiplier") label="DAGGERHEART.ACTIONS.Settings.multiplier" classes="inline-child" localize=true }} - {{formField fields.valueAlt.fields.dice value=data.valueAlt.dice name=(concat basePath ".valueAlt.dice") classes="inline-child" localize=true}} - {{formField fields.valueAlt.fields.bonus value=data.valueAlt.bonus name=(concat basePath ".valueAlt.bonus") localize=true classes="inline-child"}} -
-
- {{/if}} - +{{#*inline "hordeDamage"}} +
+ {{localize "DAGGERHEART.ACTORS.Adversary.hordeDamage"}} +
+ + {{formField fields.valueAlt.fields.flatMultiplier value=source.valueAlt.flatMultiplier name=(concat basePath ".valueAlt.flatMultiplier") label="DAGGERHEART.ACTIONS.Settings.multiplier" classes="inline-child" localize=true }} + {{formField fields.valueAlt.fields.dice value=source.valueAlt.dice name=(concat basePath ".valueAlt.dice") classes="inline-child" localize=true}} + {{formField fields.valueAlt.fields.bonus value=source.valueAlt.bonus name=(concat basePath ".valueAlt.bonus") localize=true classes="inline-child"}} +
+
{{/inline}} \ No newline at end of file diff --git a/templates/sheets-settings/adversary-settings/attack.hbs b/templates/sheets-settings/adversary-settings/attack.hbs index 41960032..f81f0477 100644 --- a/templates/sheets-settings/adversary-settings/attack.hbs +++ b/templates/sheets-settings/adversary-settings/attack.hbs @@ -22,5 +22,5 @@ {{formGroup systemFields.criticalThreshold value=document._source.system.criticalThreshold label="DAGGERHEART.ACTIONS.Settings.criticalThreshold" name="system.criticalThreshold" localize=true}} - {{> 'systems/daggerheart/templates/actionTypes/damage.hbs' fields=systemFields.attack.fields.damage.fields.parts.element.fields source=document.system.attack.damage path="system.attack." baseFields=systemFields.attack.fields.damage.fields horde=(eq document._source.system.type 'horde')}} + {{> 'systems/daggerheart/templates/actionTypes/damage.hbs' fields=systemFields.attack.fields.damage.fields source=document.system.attack.damage path="system.attack." baseFields=systemFields.attack.fields.damage.fields horde=(eq document._source.system.type 'horde')}} \ No newline at end of file From 2ef78f2c892feb3aad5c2a33563e2d6e87ee7d7e Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Sat, 18 Jul 2026 00:16:51 -0400 Subject: [PATCH 09/10] Prevent assigning main damage on healing actions --- module/data/action/baseAction.mjs | 2 +- templates/actionTypes/damage.hbs | 64 ++++++++++++++++--------------- 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index 12d2f26e..f773ae32 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -474,7 +474,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel source.damage.main = null; source.damage.resources = {}; for (const [partKey, part] of Object.entries(source.damage.parts)) { - if (partKey === 'hitPoints') { + if (partKey === 'hitPoints' && source.type !== 'healing') { source.damage.main = { ...part, includeBase: source.damage.includeBase, diff --git a/templates/actionTypes/damage.hbs b/templates/actionTypes/damage.hbs index f315f8a1..df8a71b1 100644 --- a/templates/actionTypes/damage.hbs +++ b/templates/actionTypes/damage.hbs @@ -1,44 +1,46 @@ -
- - {{#if (eq @root.source.type 'healing')}} - {{localize "DAGGERHEART.GENERAL.healing"}} - {{else}} +{{#unless (eq @root.source.type 'healing')}} +
+ {{localize "DAGGERHEART.GENERAL.damage"}} - {{/if}} - {{#if source.main}} - - {{else}} - - {{/if}} - + {{#if source.main}} + + {{else}} + + {{/if}} + - {{#if source.main}} -
- {{#if @root.hasBaseDamage}} - {{formField @root.fields.damage.fields.main.fields.includeBase value=@root.source.damage.includeBase name="damage.main.includeBase" classes="checkbox" localize=true }} + {{#if source.main}} +
+ {{#if @root.hasBaseDamage}} + {{formField @root.fields.damage.fields.main.fields.includeBase value=@root.source.damage.includeBase name="damage.main.includeBase" classes="checkbox" localize=true }} + {{/if}} + {{#unless (eq @root.source.type 'healing')}} + {{formField baseFields.main.fields.direct value=source.main.direct name=(concat path "damage.main.direct") localize=true classes="checkbox"}} + {{/unless}} + {{#if (and @root.isNPC (not (eq path 'system.attack.')))}} + {{formField baseFields.main.fields.groupAttack value=source.main.groupAttack name=(concat path "damage.main.groupAttack") localize=true classes="select"}} + {{/if}} +
+ {{> damageData damage=source.main fields=fields.main.fields basePath=(concat path "damage.main")}} + {{#if horde}} + {{> hordeDamage source=source.main fields=fields.main.fields basePath=(concat path "damage.main")}} {{/if}} - {{#unless (eq @root.source.type 'healing')}} - {{formField baseFields.main.fields.direct value=source.main.direct name=(concat path "damage.main.direct") localize=true classes="checkbox"}} - {{/unless}} - {{#if (and @root.isNPC (not (eq path 'system.attack.')))}} - {{formField baseFields.main.fields.groupAttack value=source.main.groupAttack name=(concat path "damage.main.groupAttack") localize=true classes="select"}} + {{#if (ne @root.source.type 'healing')}} + {{formField fields.main.fields.type value=source.main.type name=(concat path "damage.main.type") localize=true}} {{/if}} -
- {{> damageData damage=source.main fields=fields.main.fields basePath=(concat path "damage.main")}} - {{#if horde}} - {{> hordeDamage source=source.main fields=fields.main.fields basePath=(concat path "damage.main")}} {{/if}} - {{#if (ne @root.source.type 'healing')}} - {{formField fields.main.fields.type value=source.main.type name=(concat path "damage.main.type") localize=true}} - {{/if}} - {{/if}} -
+
+{{/unless}} {{#unless (eq path 'system.attack.')}} {{! In the future, consider allowing this even on NPCs}}
- {{localize "DAGGERHEART.GENERAL.Resource.plural"}} + {{#if (eq @root.source.type 'healing')}} + {{localize "DAGGERHEART.GENERAL.healing"}} + {{else}} + {{localize "DAGGERHEART.GENERAL.Resource.plural"}} + {{/if}} {{#unless @root.allDamageTypesUsed}}{{/unless}} {{#each source.resources as |dmg key|}} From 907bb8be506a14a0e5feca7423886dccb63d38ba Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Sat, 18 Jul 2026 01:06:58 -0400 Subject: [PATCH 10/10] Fix taking damage --- module/data/fields/action/damageField.mjs | 16 ++-- module/documents/actor.mjs | 100 +++++++++++----------- 2 files changed, 59 insertions(+), 57 deletions(-) diff --git a/module/data/fields/action/damageField.mjs b/module/data/fields/action/damageField.mjs index 4edc2533..9f382b59 100644 --- a/module/data/fields/action/damageField.mjs +++ b/module/data/fields/action/damageField.mjs @@ -80,7 +80,7 @@ export default class DamageField extends fields.SchemaField { const targetDamage = []; const damagePromises = []; - for (let target of targets) { + for (const target of targets) { const actor = foundry.utils.fromUuidSync(target.actorId); if (!actor) continue; if (!config.hasHealing && config.onSave && target.saved?.success === true) { @@ -102,14 +102,12 @@ export default class DamageField extends fields.SchemaField { actor.takeHealing(config.damage.types).then(updates => targetDamage.push({ token, updates })) ); else { - const configDamage = foundry.utils.deepClone(config.damage.types); - const hpDamageMultiplier = config.actionActor?.system.rules?.attack?.damage?.hpDamageMultiplier ?? 1; - const hpDamageTakenMultiplier = actor.system.rules?.attack?.damage?.hpDamageTakenMultiplier; - if (configDamage.hitPoints) { - configDamage.hitPoints = configDamage.hitPoints.toJSON(); - configDamage.hitPoints.total = Math.ceil( - configDamage.hitPoints.total * hpDamageMultiplier * hpDamageTakenMultiplier - ); + const configDamage = config.damage.clone(); + configDamage.main &&= configDamage.main.toJSON(); + if (configDamage.main) { + const multiplier = config.actionActor?.system.rules?.attack?.damage?.hpDamageMultiplier ?? 1; + const takenMultiplier = actor.system.rules?.attack?.damage?.hpDamageTakenMultiplier; + configDamage.main.total = Math.ceil(configDamage.main.total * multiplier * takenMultiplier); } damagePromises.push( diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index d7733dd5..5048abef 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -656,62 +656,66 @@ export default class DhpActor extends Actor { return; } - const updates = []; - - Object.entries(damages).forEach(([key, damage]) => { - if (key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) - damage.total = this.calculateDamage(damage.total, damage.damageTypes); - const update = updates.find(u => u.key === key); - if (update) { - update.value += damage.total; - update.damageTypes.add(...new Set(damage.damageTypes)); - } else updates.push({ value: damage.total, key, damageTypes: new Set(damage.damageTypes) }); - }); + if (damages.main) { + damages.main.total = this.calculateDamage(damages.main.total, damages.main.damageTypes); + } if (Hooks.call(`${CONFIG.DH.id}.postCalculateDamage`, this, damages) === false) return null; - if (!updates.length) return; + // Convert deducted resources and damage to a record of updates, merging damage to hp with hp marked + const updates = []; + for (const [key, damage] of Object.entries(damages.resources)) { + updates.push({ key, value: damage.total }); + } + if (damages.main) { + const existing = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id); + const value = this.convertDamageToThreshold(damages.main.total) + (existing?.value ?? 0); + const damageTypes = new Set(damages.main.options.damageTypes); + if (existing) { + existing.value = value; + existing.damageTypes = damageTypes; + } else { + updates.push({ value, damageTypes, key: CONFIG.DH.GENERAL.healingTypes.hitPoints.id }); + } + } + if (!updates.some(u => u.value !== 0)) return; // early return if nothing to do const hpDamage = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id); - if (hpDamage?.value) { - hpDamage.value = this.convertDamageToThreshold(hpDamage.value); - if (this.type === 'character' && !isDirect && this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes)) { - const armorSlotResult = await this.owner.query( - 'armorSlot', - { - actorId: this.uuid, - damage: hpDamage.value, - type: [...hpDamage.damageTypes] - }, - { - timeout: 30000 - } - ); - if (armorSlotResult) { - const { modifiedDamage, armorChanges, stressSpent } = armorSlotResult; - updates.find(u => u.key === 'hitPoints').value = modifiedDamage; - for (const armorChange of armorChanges) { - updates.push({ value: armorChange.amount, key: 'armor', uuid: armorChange.uuid }); - } - if (stressSpent) { - const stressUpdate = updates.find(u => u.key === 'stress'); - if (stressUpdate) stressUpdate.value += stressSpent; - else updates.push({ value: stressSpent, key: 'stress' }); - } + if (hpDamage && this.type === 'character' && !isDirect && this.#canReduceDamage(hpDamage.total, hpDamage.damageTypes)) { + const armorSlotResult = await this.owner.query( + 'armorSlot', + { + actorId: this.uuid, + damage: hpDamage.value, + type: [...hpDamage.damageTypes] + }, + { + timeout: 30000 + } + ); + if (armorSlotResult) { + const { modifiedDamage, armorChanges, stressSpent } = armorSlotResult; + hpDamage.value = modifiedDamage; + for (const armorChange of armorChanges) { + updates.push({ value: armorChange.amount, key: 'armor', uuid: armorChange.uuid }); + } + if (stressSpent) { + const stressUpdate = updates.find(u => u.key === 'stress'); + if (stressUpdate) stressUpdate.value += stressSpent; + else updates.push({ value: stressSpent, key: 'stress' }); } } - if (this.type === 'adversary') { - const reducedSeverity = hpDamage.damageTypes.reduce((value, curr) => { - return Math.max(this.system.rules.damageReduction.reduceSeverity[curr], value); - }, 0); - hpDamage.value = Math.max(hpDamage.value - reducedSeverity, 0); + } else if (hpDamage && this.type === 'adversary') { + const reducedSeverity = hpDamage.damageTypes.reduce((value, curr) => { + return Math.max(this.system.rules.damageReduction.reduceSeverity[curr], value); + }, 0); + hpDamage.value = Math.max(hpDamage.value - reducedSeverity, 0); - if ( - hpDamage.value && - this.system.rules.damageReduction.thresholdImmunities[getDamageKey(hpDamage.value)] - ) { - hpDamage.value -= 1; - } + if ( + hpDamage.value && + this.system.rules.damageReduction.thresholdImmunities[getDamageKey(hpDamage.value)] + ) { + hpDamage.value -= 1; } }