From fad64c9a35dcfb0c8b6288ff3652c82df81be4b2 Mon Sep 17 00:00:00 2001 From: Dapoulp <74197441+Dapoulp@users.noreply.github.com> Date: Mon, 28 Jul 2025 00:11:43 +0200 Subject: [PATCH] Feature/allow action healing multiple resources (#437) * Healing updates * Remove comments --- lang/en.json | 10 ++++-- module/applications/dialogs/damageDialog.mjs | 8 ++--- .../dialogs/damageSelectionDialog.mjs | 2 ++ .../sheets-configs/action-config.mjs | 1 - module/applications/ui/chatLog.mjs | 33 +++---------------- module/config/generalConfig.mjs | 2 +- module/data/action/baseAction.mjs | 4 +-- module/data/action/damageAction.mjs | 3 +- module/data/action/healingAction.mjs | 13 ++++++-- module/data/chat-message/damageRoll.mjs | 1 + module/data/fields/action/healingField.mjs | 7 ++-- module/data/fields/action/targetField.mjs | 12 ++++--- module/data/fields/actionField.mjs | 7 ++-- module/dice/damageRoll.mjs | 22 +++++++------ module/documents/actor.mjs | 31 ++++++++++++----- module/helpers/utils.mjs | 15 ++++----- templates/actionTypes/damage.hbs | 8 +++-- .../dialogs/dice-roll/damageSelection.hbs | 16 +++++---- .../action-settings/effect.hbs | 2 +- templates/ui/chat/damage-roll.hbs | 1 + templates/ui/chat/duality-roll.hbs | 30 +++++++++++++---- 21 files changed, 130 insertions(+), 98 deletions(-) diff --git a/lang/en.json b/lang/en.json index a5f145ad..a472b40e 100755 --- a/lang/en.json +++ b/lang/en.json @@ -713,7 +713,7 @@ "abbreviation": "HO" }, "armorStack": { - "name": "Armor Stack", + "name": "Armor Slot", "abbreviation": "AS" }, "fear": { @@ -1029,6 +1029,9 @@ }, "damageRoll": { "name": "Damage Roll" + }, + "healingRoll": { + "name": "Healing Roll" } }, "Duration": { @@ -1671,8 +1674,9 @@ "subclassFeatureTitle": "Subclass Feature" }, "healingRoll": { - "title": "Heal - {healing}", - "heal": "Heal" + "title": "Heal - {damage}", + "heal": "Heal", + "applyHealing": "Apply Healing" }, "reroll": { "confirmTitle": "Reroll Dice", diff --git a/module/applications/dialogs/damageDialog.mjs b/module/applications/dialogs/damageDialog.mjs index 78452054..cffa6433 100644 --- a/module/applications/dialogs/damageDialog.mjs +++ b/module/applications/dialogs/damageDialog.mjs @@ -38,17 +38,15 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application }; get title() { - return game.i18n.localize('DAGGERHEART.EFFECTS.ApplyLocations.damageRoll.name'); + return game.i18n.localize(`DAGGERHEART.EFFECTS.ApplyLocations.${this.config.isHealing ? 'healing' : 'damage'}Roll.name`); } async _prepareContext(_options) { const context = await super._prepareContext(_options); context.config = CONFIG.DH; - context.title = this.config.title - ? this.config.title - : game.i18n.localize('DAGGERHEART.EFFECTS.ApplyLocations.damageRoll.name'); - // context.extraFormula = this.config.extraFormula; + context.title = this.config.title ?? this.title; context.formula = this.roll.constructFormula(this.config); + context.isHealing = this.config.isHealing; context.directDamage = this.config.directDamage; context.selectedRollMode = this.config.selectedRollMode; context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({ diff --git a/module/applications/dialogs/damageSelectionDialog.mjs b/module/applications/dialogs/damageSelectionDialog.mjs index 547ba87c..3d4b312c 100644 --- a/module/applications/dialogs/damageSelectionDialog.mjs +++ b/module/applications/dialogs/damageSelectionDialog.mjs @@ -1,3 +1,5 @@ +// TO DELETE ? + const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; export default class DamageSelectionDialog extends HandlebarsApplicationMixin(ApplicationV2) { diff --git a/module/applications/sheets-configs/action-config.mjs b/module/applications/sheets-configs/action-config.mjs index ed81b7e9..7d50c0c6 100644 --- a/module/applications/sheets-configs/action-config.mjs +++ b/module/applications/sheets-configs/action-config.mjs @@ -118,7 +118,6 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { { key: 1, label: game.i18n.localize('DAGGERHEART.GENERAL.Tiers.1') }, ...Object.values(settingsTiers).map(x => ({ key: x.tier, label: x.name })) ]; - return context; } diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index 8af11249..e0f990ba 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -17,9 +17,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo html.querySelectorAll('.duality-action-damage').forEach(element => element.addEventListener('click', event => this.onRollDamage(event, data.message)) ); - html.querySelectorAll('.duality-action-healing').forEach(element => - element.addEventListener('click', event => this.onRollHealing(event, data.message)) - ); html.querySelectorAll('.target-save-container').forEach(element => element.addEventListener('click', event => this.onRollSave(event, data.message)) ); @@ -92,17 +89,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo } } - async onRollHealing(event, message) { - event.stopPropagation(); - const actor = await this.getActor(message.system.source.actor); - if (!actor || !game.user.isGM) return true; - if (message.system.source.item && message.system.source.action) { - const action = this.getAction(actor, message.system.source.item, message.system.source.action); - if (!action || !action?.rollHealing) return; - await action.rollHealing(event, message); - } - } - async onRollSave(event, message) { event.stopPropagation(); const actor = await this.getActor(message.system.source.actor), @@ -160,7 +146,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo return { isHit, targets: isHit - ? message.system.targets.filter(t => t.hit === true).map(target => game.canvas.tokens.get(target.id)) + ? message.system.targets.filter(t => t.hit === true).map(target => game.canvas.tokens.documentCollection.find(t => t.actor.uuid === target.actorId)) : Array.from(game.user.targets) }; } @@ -222,19 +208,10 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo }); } - target.actor.takeDamage(damages); - } - } - - async onHealing(event, message) { - event.stopPropagation(); - const targets = Array.from(game.user.targets); - - if (targets.length === 0) - return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected')); - - for (var target of targets) { - target.actor.takeHealing(message.system.roll); + if(message.system.hasHealing) + target.actor.takeHealing(damages); + else + target.actor.takeDamage(damages); } } diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index fff7b613..6781dc83 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -439,7 +439,7 @@ export const abilityCosts = { }, armor: { id: 'armor', - label: 'Armor Stack', + label: 'Armor Slot', group: 'TYPES.Actor.character' }, fear: { diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index 67e0e00b..f3fdb3d6 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -169,8 +169,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel }, dialog: {}, type: this.type, - hasDamage: !!this.damage?.parts?.length, - hasHealing: !!this.healing, + hasDamage: this.damage?.parts?.length && this.type !== 'healing', + hasHealing: this.damage?.parts?.length && this.type === 'healing', hasEffect: !!this.effects?.length, hasSave: this.hasSave, selectedRollMode: game.settings.get('core', 'rollMode'), diff --git a/module/data/action/damageAction.mjs b/module/data/action/damageAction.mjs index cb813114..c8da5737 100644 --- a/module/data/action/damageAction.mjs +++ b/module/data/action/damageAction.mjs @@ -47,11 +47,12 @@ export default class DHDamageAction extends DHBaseAction { formulas = this.formatFormulas(formulas, systemData); const config = { - title: game.i18n.format('DAGGERHEART.UI.Chat.damageRoll.title', { damage: game.i18n.localize(this.name) }), + title: game.i18n.format(`DAGGERHEART.UI.Chat.${ this.type === 'healing' ? 'healing' : 'damage'}Roll.title`, { damage: game.i18n.localize(this.name) }), roll: formulas, targets: systemData.targets?.filter(t => t.hit) ?? data.targets, hasSave: this.hasSave, isCritical: systemData.roll?.isCritical ?? false, + isHealing: this.type === 'healing', source: systemData.source, data: this.getRollData(), event diff --git a/module/data/action/healingAction.mjs b/module/data/action/healingAction.mjs index 48aa4146..dad67dec 100644 --- a/module/data/action/healingAction.mjs +++ b/module/data/action/healingAction.mjs @@ -1,7 +1,14 @@ import DHBaseAction from './baseAction.mjs'; +import DHDamageAction from './damageAction.mjs'; -export default class DHHealingAction extends DHBaseAction { - static extraSchemas = [...super.extraSchemas, 'target', 'effects', 'healing', 'roll']; +export default class DHHealingAction extends DHDamageAction { + static extraSchemas = [...super.extraSchemas, 'roll']; + + static getRollType(parent) { + return 'spellcast'; + } + + /* static extraSchemas = [...super.extraSchemas, 'target', 'effects', 'healing', 'roll']; static getRollType(parent) { return 'spellcast'; @@ -44,5 +51,5 @@ export default class DHHealingAction extends DHBaseAction { get modifiers() { return []; - } + } */ } diff --git a/module/data/chat-message/damageRoll.mjs b/module/data/chat-message/damageRoll.mjs index 210cc0fe..167e97d9 100644 --- a/module/data/chat-message/damageRoll.mjs +++ b/module/data/chat-message/damageRoll.mjs @@ -21,6 +21,7 @@ export default class DHDamageRoll extends foundry.abstract.TypeDataModel { ), targetSelection: new fields.BooleanField({ initial: true }), hasSave: new fields.BooleanField({ initial: false }), + isHealing: new fields.BooleanField({ initial: false }), onSave: new fields.StringField(), source: new fields.SchemaField({ actor: new fields.StringField(), diff --git a/module/data/fields/action/healingField.mjs b/module/data/fields/action/healingField.mjs index 3734e596..98f4f5ea 100644 --- a/module/data/fields/action/healingField.mjs +++ b/module/data/fields/action/healingField.mjs @@ -2,8 +2,11 @@ import { DHDamageData } from './damageField.mjs'; const fields = foundry.data.fields; -export default class HealingField extends fields.EmbeddedDataField { +export default class HealingField extends fields.SchemaField { constructor(options, context = {}) { - super(DHDamageData, options, context); + const healingFields = { + parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)) + }; + super(healingFields, options, context); } } diff --git a/module/data/fields/action/targetField.mjs b/module/data/fields/action/targetField.mjs index 41f931a6..1024d5d7 100644 --- a/module/data/fields/action/targetField.mjs +++ b/module/data/fields/action/targetField.mjs @@ -17,11 +17,13 @@ export default class TargetField extends fields.SchemaField { if (!this.target?.type) return []; let targets; if (this.target?.type === CONFIG.DH.ACTIONS.targetTypes.self.id) - targets = TargetField.formatTarget.call(this, this.actor.token ?? this.actor.prototypeToken); - targets = Array.from(game.user.targets); - if (this.target.type !== CONFIG.DH.ACTIONS.targetTypes.any.id) { - targets = targets.filter(t => TargetField.isTargetFriendly.call(this, t)); - if (this.target.amount && targets.length > this.target.amount) targets = []; + targets = [this.actor.token ?? this.actor.prototypeToken]; + else { + targets = Array.from(game.user.targets); + if (this.target.type !== CONFIG.DH.ACTIONS.targetTypes.any.id) { + targets = targets.filter(t => TargetField.isTargetFriendly.call(this, t)); + if (this.target.amount && targets.length > this.target.amount) targets = []; + } } config.targets = targets.map(t => TargetField.formatTarget.call(this, t)); const hasTargets = TargetField.checkTargets.call(this, this.target.amount, config.targets); diff --git a/module/data/fields/actionField.mjs b/module/data/fields/actionField.mjs index 287d53ee..8cf7b30c 100644 --- a/module/data/fields/actionField.mjs +++ b/module/data/fields/actionField.mjs @@ -200,7 +200,7 @@ export function ActionMixin(Base) { } ); const created = await parent.parent.update({ [`system.actions.${action.id}`]: action.toObject() }); - const newAction = parent.actions.get(action.id); + const newAction = created.system.actions.get(action.id); if (!newAction) return null; if (renderSheet) newAction.sheet.render({ force: true }); return newAction; @@ -215,10 +215,7 @@ export function ActionMixin(Base) { await this.parent.updateSource({ [path]: updates }, options); result = this.parent; } else { - /* Fix me - For some reason updating the "healing" section in particular doesn't work without this */ - await this.item.update({ [path]: updates }, options); - await this.item.updateSource({ [path]: updates }, options); - result = this.item; + result = await this.item.update({ [path]: updates }, options); } return this.inCollection diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index 48313c5b..43e275d4 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -83,18 +83,20 @@ export default class DamageRoll extends DHRoll { applyBaseBonus(part) { const modifiers = [], - type = this.options.messageType ?? 'damage', + type = this.options.messageType ?? (this.options.isHealing ? 'healing' : 'damage'), options = part ?? this.options; - + modifiers.push(...this.getBonus(`${type}`, `${type.capitalize()} Bonus`)); - options.damageTypes?.forEach(t => { - modifiers.push(...this.getBonus(`${type}.${t}`, `${t.capitalize()} ${type.capitalize()} Bonus`)); - }); - const weapons = ['primaryWeapon', 'secondaryWeapon']; - weapons.forEach(w => { - if (this.options.source.item && this.options.source.item === this.data[w]?.id) - modifiers.push(...this.getBonus(`${type}.${w}`, 'Weapon Bonus')); - }); + if(!this.options.isHealing) { + options.damageTypes?.forEach(t => { + modifiers.push(...this.getBonus(`${type}.${t}`, `${t.capitalize()} ${type.capitalize()} Bonus`)); + }); + const weapons = ['primaryWeapon', 'secondaryWeapon']; + weapons.forEach(w => { + if (this.options.source.item && this.options.source.item === this.data[w]?.id) + modifiers.push(...this.getBonus(`${type}.${w}`, 'Weapon Bonus')); + }); + } return modifiers; } diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 5a26ded6..fc62e870 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -471,7 +471,7 @@ export default class DhpActor extends Actor { await this.modifyResource(updates); - if (Hooks.call(`${CONFIG.DH.id}.postTakeDamage`, this, damages) === false) return null; + if (Hooks.call(`${CONFIG.DH.id}.postTakeDamage`, this, updates) === false) return null; } calculateDamage(baseDamage, type) { @@ -498,14 +498,28 @@ export default class DhpActor extends Actor { return reduction === Infinity ? 0 : reduction; } - async takeHealing(resources) { - const updates = Object.entries(resources).map(([key, value]) => ({ - key: key, - value: !(key === 'fear' || this.system?.resources?.[key]?.isReversed === false) - ? value.total * -1 - : value.total - })); + async takeHealing(healings) { + if (Hooks.call(`${CONFIG.DH.id}.preTakeHealing`, this, healings) === false) return null; + + const updates = []; + Object.entries(healings).forEach(([key, healing]) => { + healing.parts.forEach(part => { + const update = updates.find(u => u.key === key); + if (update) + update.value += part.total; + else updates.push({ value: part.total, key }); + }); + }); + + updates.forEach( + u => + (u.value = + !(u.key === 'fear' || this.system?.resources?.[u.key]?.isReversed === false) ? u.value * -1 : u.value) + ); + await this.modifyResource(updates); + + if (Hooks.call(`${CONFIG.DH.id}.postTakeHealing`, this, updates) === false) return null; } async modifyResource(resources) { @@ -547,6 +561,7 @@ export default class DhpActor extends Actor { } } }); + Object.keys(updates).forEach(async key => { const u = updates[key]; if (key === 'items') { diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 30f360cf..ac9ac9e7 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -56,14 +56,13 @@ export const getCommandTarget = (options = {}) => { }; export const setDiceSoNiceForDualityRoll = async (rollResult, advantageState, hopeFaces, fearFaces, advantageFaces) => { - if (game.modules.get('dice-so-nice')?.active) { - const diceSoNicePresets = await getDiceSoNicePresets(hopeFaces, fearFaces, advantageFaces, advantageFaces); - rollResult.dice[0].options = diceSoNicePresets.hope; - rollResult.dice[1].options = diceSoNicePresets.fear; - if (rollResult.dice[2] && advantageState) { - rollResult.dice[2].options = - advantageState === 1 ? diceSoNicePresets.advantage : diceSoNicePresets.disadvantage; - } + if (!game.modules.get('dice-so-nice')?.active) return; + const diceSoNicePresets = await getDiceSoNicePresets(hopeFaces, fearFaces, advantageFaces, advantageFaces); + rollResult.dice[0].options = diceSoNicePresets.hope; + rollResult.dice[1].options = diceSoNicePresets.fear; + if (rollResult.dice[2] && advantageState) { + rollResult.dice[2].options = + advantageState === 1 ? diceSoNicePresets.advantage : diceSoNicePresets.disadvantage; } }; diff --git a/templates/actionTypes/damage.hbs b/templates/actionTypes/damage.hbs index 9ae25e0e..2cf71abd 100644 --- a/templates/actionTypes/damage.hbs +++ b/templates/actionTypes/damage.hbs @@ -1,7 +1,11 @@