From 5aa9ba661a33e35a06828ad0f30bcf556b2e2b77 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:10:38 +0200 Subject: [PATCH 1/4] 286 - Adversary Experience Value Bug (#293) * Changed experience schema field name from 'modifier' to 'total' to match useage in Character and Companion * Using 'signedString()' --- module/applications/sheets/actors/adversary.mjs | 4 +--- module/data/actor/adversary.mjs | 4 ++-- templates/sheets-settings/adversary-settings/experiences.hbs | 2 +- templates/sheets/actors/adversary/sidebar.hbs | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/module/applications/sheets/actors/adversary.mjs b/module/applications/sheets/actors/adversary.mjs index 5dae9741..67f57781 100644 --- a/module/applications/sheets/actors/adversary.mjs +++ b/module/applications/sheets/actors/adversary.mjs @@ -92,9 +92,7 @@ export default class AdversarySheet extends DHBaseActorSheet { const cls = getDocumentClass('ChatMessage'); const systemData = { name: game.i18n.localize('DAGGERHEART.GENERAL.Experience.single'), - description: `${experience.name} ${ - experience.modifier < 0 ? experience.modifier : `+${experience.modifier}` - }` + description: `${experience.name} ${experience.total.signedString()}` }; const msg = new cls({ type: 'abilityUse', diff --git a/module/data/actor/adversary.mjs b/module/data/actor/adversary.mjs index 3cbf4eaa..9bb5d5f8 100644 --- a/module/data/actor/adversary.mjs +++ b/module/data/actor/adversary.mjs @@ -15,7 +15,7 @@ export default class DhpAdversary extends BaseDataActor { return foundry.utils.mergeObject(super.metadata, { label: 'TYPES.Actor.adversary', type: 'adversary', - settingSheet: DHAdversarySettings, + settingSheet: DHAdversarySettings }); } @@ -74,7 +74,7 @@ export default class DhpAdversary extends BaseDataActor { experiences: new fields.TypedObjectField( new fields.SchemaField({ name: new fields.StringField(), - modifier: new fields.NumberField({ required: true, integer: true, initial: 1 }) + total: new fields.NumberField({ required: true, integer: true, initial: 1 }) }) ), bonuses: new fields.SchemaField({ diff --git a/templates/sheets-settings/adversary-settings/experiences.hbs b/templates/sheets-settings/adversary-settings/experiences.hbs index fb758a40..c15bf6b9 100644 --- a/templates/sheets-settings/adversary-settings/experiences.hbs +++ b/templates/sheets-settings/adversary-settings/experiences.hbs @@ -13,7 +13,7 @@ {{#each document.system.experiences as |experience key|}}
  • - +
  • {{/each}} diff --git a/templates/sheets/actors/adversary/sidebar.hbs b/templates/sheets/actors/adversary/sidebar.hbs index 8a410348..b26c1b81 100644 --- a/templates/sheets/actors/adversary/sidebar.hbs +++ b/templates/sheets/actors/adversary/sidebar.hbs @@ -94,7 +94,7 @@ {{#each source.system.experiences as |experience id|}}
    - +{{experience.modifier}} + +{{experience.total}}
    {{experience.name}}
    From 61f04df765102dfb9b25d7d11042dfdbc160ae51 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Tue, 8 Jul 2025 20:04:54 +0200 Subject: [PATCH 2/4] 289 - Confirm Delete Dialogs (#298) * Added confirm dialogs to delete * Localization fix --- lang/en.json | 4 +++ .../sheets-configs/adversary-settings.mjs | 12 +++++++++ .../sheets-configs/environment-settings.mjs | 22 ++++++++++++--- .../applications/sheets/actors/character.mjs | 22 ++++++++++++--- .../applications/sheets/api/actor-setting.mjs | 5 +++- .../sheets/api/application-mixin.mjs | 19 +++++++++++-- module/applications/sheets/api/base-item.mjs | 27 +++++++++++++++++++ 7 files changed, 101 insertions(+), 10 deletions(-) diff --git a/lang/en.json b/lang/en.json index 857a9387..75c8cfa7 100755 --- a/lang/en.json +++ b/lang/en.json @@ -213,6 +213,10 @@ "encounter": "Encounter" } }, + "DeleteConfirmation": { + "title": "Delete {type} - {name}", + "text": "Are you sure you want to delete {name}?" + }, "DamageReduction": { "armorMarks": "Armor Marks", "armorWithStress": "Spend 1 stress to use an extra mark", diff --git a/module/applications/sheets-configs/adversary-settings.mjs b/module/applications/sheets-configs/adversary-settings.mjs index d95e6129..57deea25 100644 --- a/module/applications/sheets-configs/adversary-settings.mjs +++ b/module/applications/sheets-configs/adversary-settings.mjs @@ -70,6 +70,18 @@ export default class DHAdversarySettings extends DHBaseActorSettings { * @type {ApplicationClickAction} */ static async #removeExperience(_, target) { + const experience = this.actor.system.experiences[target.dataset.experience]; + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { + type: game.i18n.localize(`DAGGERHEART.GENERAL.Experience.single`), + name: experience.name + }) + }, + content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: experience.name }) + }); + if (!confirmed) return; + await this.actor.update({ [`system.experiences.-=${target.dataset.experience}`]: null }); } diff --git a/module/applications/sheets-configs/environment-settings.mjs b/module/applications/sheets-configs/environment-settings.mjs index d0ca897a..7422f5fc 100644 --- a/module/applications/sheets-configs/environment-settings.mjs +++ b/module/applications/sheets-configs/environment-settings.mjs @@ -82,10 +82,24 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings { static async #deleteAdversary(event, target) { const adversaryKey = target.dataset.adversary; const path = `system.potentialAdversaries.${target.dataset.potentialAdversary}.adversaries`; - console.log(target.dataset.potentialAdversar); - const newAdversaries = foundry.utils - .getProperty(this.actor, path) - .filter(x => x && (x?.uuid ?? x) !== adversaryKey); + const property = foundry.utils.getProperty(this.actor, path); + const adversary = property.find(x => (x?.uuid ?? x) === adversaryKey); + + if (adversary) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { + type: game.i18n.localize('TYPES.Actor.adversary'), + name: adversary.name + }) + }, + content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: adversary.name }) + }); + + if (!confirmed) return; + } + + const newAdversaries = property.filter(x => x && (x?.uuid ?? x) !== adversaryKey); await this.actor.update({ [path]: newAdversaries }); } diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 02e693d7..a77fe71f 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -249,7 +249,23 @@ export default class CharacterSheet extends DHBaseActorSheet { { name: 'DAGGERHEART.Sheets.PC.ContextMenu.Delete', icon: '', - callback: el => getItem(el).delete() + callback: async el => { + const item = getItem(el); + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { + type: game.i18n.localize(`TYPES.${item.documentName}.${item.type}`), + name: item.name + }) + }, + content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { + name: item.name + }) + }); + if (!confirmed) return; + + item.delete(); + } } ]; } @@ -372,7 +388,7 @@ export default class CharacterSheet extends DHBaseActorSheet { li.hidden = !(menu.has(item.id) && matchesSearch); } } - + /* -------------------------------------------- */ /* Filter Menus */ /* -------------------------------------------- */ @@ -495,7 +511,7 @@ export default class CharacterSheet extends DHBaseActorSheet { const config = { event: event, title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`, - headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilitychecktitle', { + headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { ability: abilityLabel }), roll: { diff --git a/module/applications/sheets/api/actor-setting.mjs b/module/applications/sheets/api/actor-setting.mjs index 50e2b0a9..79aa1c37 100644 --- a/module/applications/sheets/api/actor-setting.mjs +++ b/module/applications/sheets/api/actor-setting.mjs @@ -42,9 +42,12 @@ export default class DHBaseActorSettings extends DHApplicationMixin(DocumentShee /**@inheritdoc */ async _prepareContext(options) { const context = await super._prepareContext(options); - context.systemFields.attack.fields = this.actor.system.attack.schema.fields; context.isNPC = this.actor.isNPC; + if (context.systemFields.attack) { + context.systemFields.attack.fields = this.actor.system.attack.schema.fields; + } + return context; } } diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index d0ef63d6..0e5f211d 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -218,7 +218,6 @@ export default function DHApplicationMixin(Base) { */ static async #createDoc(event, button) { const { documentClass, type } = button.dataset; - console.log(documentClass, type); const parent = this.document; const cls = getDocumentClass(documentClass); @@ -250,7 +249,23 @@ export default function DHApplicationMixin(Base) { */ static async #deleteDoc(_event, button) { const { type, docId } = button.dataset; - await this.document.getEmbeddedDocument(type, docId, { strict: true }).delete(); + const document = this.document.getEmbeddedDocument(type, docId, { strict: true }); + const typeName = game.i18n.localize( + document.type === 'base' ? `DOCUMENT.${type}` : `TYPES.${type}.${document.type}` + ); + + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { + type: typeName, + name: document.name + }) + }, + content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: document.name }) + }); + if (!confirmed) return; + + await document.delete(); } } diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs index f9d52025..d624acc6 100644 --- a/module/applications/sheets/api/base-item.mjs +++ b/module/applications/sheets/api/base-item.mjs @@ -139,6 +139,19 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { static async #removeAction(event, button) { event.stopPropagation(); const actionIndex = button.closest('[data-index]').dataset.index; + const action = this.document.system.actions[actionIndex]; + + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { + type: game.i18n.localize(`DAGGERHEART.GENERAL.Action.single`), + name: action.name + }) + }, + content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: action.name }) + }); + if (!confirmed) return; + await this.document.update({ 'system.actions': this.document.system.actions.filter((_, index) => index !== Number.parseInt(actionIndex)) }); @@ -180,6 +193,20 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { static async #removeFeature(event, button) { event.stopPropagation(); const target = button.closest('.feature-item'); + const feature = this.document.system.features.find(x => x && x.id === target.id); + + if (feature) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { + type: game.i18n.localize(`TYPES.Item.feature`), + name: feature.name + }) + }, + content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: feature.name }) + }); + if (!confirmed) return; + } await this.document.update({ 'system.features': this.document.system.features From 861dfd977dbae69ccd840196ea4e8902e2ce1f02 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Tue, 8 Jul 2025 21:01:28 +0200 Subject: [PATCH 3/4] Beastform Improvements (#294) * BeastformEffect is editable. Added SubjectTexture field. * Using handlebars disabled helper --- lang/en.json | 1 + .../applications/sheets/actors/character.mjs | 12 +++- .../applications/sheets/items/beastform.mjs | 13 +++- module/data/activeEffect/beastformEffect.mjs | 10 +++ module/data/item/beastform.mjs | 65 ++++++++++++------- templates/sheets/global/tabs/tab-effects.hbs | 2 +- templates/sheets/items/beastform/settings.hbs | 10 +-- 7 files changed, 81 insertions(+), 32 deletions(-) diff --git a/lang/en.json b/lang/en.json index 75c8cfa7..797781d8 100755 --- a/lang/en.json +++ b/lang/en.json @@ -1129,6 +1129,7 @@ "examples": { "label": "Examples" }, "advantageOn": { "label": "Gain Advantage On" }, "tokenImg": { "label": "Token Image" }, + "tokenRingImg": { "label": "Subject Texture" }, "tokenSize": { "placeholder": "Using character dimensions", "height": { "label": "Height" }, diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index a77fe71f..c24bcfec 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -188,7 +188,17 @@ export default class CharacterSheet extends DHBaseActorSheet { * @param {HTMLElement} el * @returns {foundry.documents.Item?} */ - const getItem = el => this.actor.items.get(el.closest('[data-item-id]')?.dataset.itemId); + const getItem = element => { + const listElement = (element.target ?? element).closest('[data-item-id]'); + const itemId = listElement.dataset.itemId; + + switch (listElement.dataset.type) { + case 'effect': + return this.document.effects.get(itemId); + default: + return this.document.items.get(itemId); + } + }; return [ { diff --git a/module/applications/sheets/items/beastform.mjs b/module/applications/sheets/items/beastform.mjs index e3b72d01..194f3ab1 100644 --- a/module/applications/sheets/items/beastform.mjs +++ b/module/applications/sheets/items/beastform.mjs @@ -30,8 +30,17 @@ export default class BeastformSheet extends DHBaseItemSheet { }; /**@inheritdoc */ - async _preparePartContext(partId, context) { - await super._preparePartContext(partId, context); + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + + context.document = context.document.toObject(); + context.document.effects = this.document.effects.map(effect => { + const data = effect.toObject(); + data.id = effect.id; + if (effect.type === 'beastform') data.mandatory = true; + + return data; + }); return context; } diff --git a/module/data/activeEffect/beastformEffect.mjs b/module/data/activeEffect/beastformEffect.mjs index 3aa25bef..6445f65d 100644 --- a/module/data/activeEffect/beastformEffect.mjs +++ b/module/data/activeEffect/beastformEffect.mjs @@ -10,6 +10,11 @@ export default class BeastformEffect extends foundry.abstract.TypeDataModel { base64: false, nullable: true }), + tokenRingImg: new fields.FilePathField({ + initial: 'icons/svg/mystery-man.svg', + categories: ['IMAGE'], + base64: false + }), tokenSize: new fields.SchemaField({ height: new fields.NumberField({ integer: true, nullable: true }), width: new fields.NumberField({ integer: true, nullable: true }) @@ -28,6 +33,11 @@ export default class BeastformEffect extends foundry.abstract.TypeDataModel { width: this.characterTokenData.tokenSize.width, texture: { src: this.characterTokenData.tokenImg + }, + ring: { + subject: { + texture: this.characterTokenData.tokenRingImg + } } }; diff --git a/module/data/item/beastform.mjs b/module/data/item/beastform.mjs index 2eb871ec..b7ea5cb9 100644 --- a/module/data/item/beastform.mjs +++ b/module/data/item/beastform.mjs @@ -3,7 +3,7 @@ import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayFie import BaseDataItem from './base.mjs'; export default class DHBeastform extends BaseDataItem { - static LOCALIZATION_PREFIXES = ['DAGGERHEART.Sheets.Beastform']; + static LOCALIZATION_PREFIXES = ['DAGGERHEART.ITEMS.Beastform']; /** @inheritDoc */ static get metadata() { @@ -29,12 +29,17 @@ export default class DHBeastform extends BaseDataItem { categories: ['IMAGE'], base64: false }), + tokenRingImg: new fields.FilePathField({ + initial: 'icons/svg/mystery-man.svg', + categories: ['IMAGE'], + base64: false + }), tokenSize: new fields.SchemaField({ height: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }), width: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }) }), examples: new fields.StringField(), - advantageOn: new fields.ArrayField(new fields.StringField()), + advantageOn: new fields.StringField(), features: new ForeignDocumentUUIDArrayField({ type: 'Item' }) }; } @@ -56,40 +61,54 @@ export default class DHBeastform extends BaseDataItem { 'Item', this.features.map(x => x.toObject()) ); - const effects = await this.parent.parent.createEmbeddedDocuments( + + const extraEffects = await this.parent.parent.createEmbeddedDocuments( 'ActiveEffect', - this.parent.effects.map(x => x.toObject()) + this.parent.effects.filter(x => x.type !== 'beastform').map(x => x.toObject()) ); - await this.parent.parent.createEmbeddedDocuments('ActiveEffect', [ - { - type: 'beastform', - name: game.i18n.localize('DAGGERHEART.ITEMS.Beastform.beastformEffect'), - img: 'icons/creatures/abilities/paw-print-pair-purple.webp', - system: { - isBeastform: true, - characterTokenData: { - tokenImg: this.parent.parent.prototypeToken.texture.src, - tokenSize: { - height: this.parent.parent.prototypeToken.height, - width: this.parent.parent.prototypeToken.width - } - }, - advantageOn: this.advantageOn, - featureIds: features.map(x => x.id), - effectIds: effects.map(x => x.id) - } + const beastformEffect = this.parent.effects.find(x => x.type === 'beastform'); + await beastformEffect.updateSource({ + system: { + characterTokenData: { + tokenImg: this.parent.parent.prototypeToken.texture.src, + tokenRingImg: this.parent.parent.prototypeToken.ring.subject.texture, + tokenSize: { + height: this.parent.parent.prototypeToken.height, + width: this.parent.parent.prototypeToken.width + } + }, + advantageOn: this.advantageOn, + featureIds: features.map(x => x.id), + effectIds: extraEffects.map(x => x.id) } - ]); + }); + + await this.parent.parent.createEmbeddedDocuments('ActiveEffect', [beastformEffect.toObject()]); await updateActorTokens(this.parent.parent, { height: this.tokenSize.height, width: this.tokenSize.width, texture: { src: this.tokenImg + }, + ring: { + subject: { + texture: this.tokenRingImg + } } }); return false; } + + _onCreate() { + this.parent.createEmbeddedDocuments('ActiveEffect', [ + { + type: 'beastform', + name: game.i18n.localize('DAGGERHEART.ITEMS.Beastform.beastformEffect'), + img: 'icons/creatures/abilities/paw-print-pair-purple.webp' + } + ]); + } } diff --git a/templates/sheets/global/tabs/tab-effects.hbs b/templates/sheets/global/tabs/tab-effects.hbs index 744ef8e1..a75f1b0b 100644 --- a/templates/sheets/global/tabs/tab-effects.hbs +++ b/templates/sheets/global/tabs/tab-effects.hbs @@ -17,7 +17,7 @@ {{effect.name}}
    - +
    {{/each}} diff --git a/templates/sheets/items/beastform/settings.hbs b/templates/sheets/items/beastform/settings.hbs index 78af825f..dec7d134 100644 --- a/templates/sheets/items/beastform/settings.hbs +++ b/templates/sheets/items/beastform/settings.hbs @@ -8,11 +8,7 @@ {{formGroup systemFields.examples value=source.system.examples localize=true}}
    -
    - {{localize "DAGGERHEART.ITEMS.Beastform.FIELDS.advantageOn.label"}} - - {{!-- {{formGroup systemFields.examples value=source.system.examples localize=true}} --}} -
    + {{formGroup systemFields.advantageOn value=source.system.advantageOn localize=true}}
    {{localize "DAGGERHEART.ITEMS.Beastform.tokenTitle"}} @@ -21,6 +17,10 @@ {{formGroup systemFields.tokenImg value=source.system.tokenImg localize=true}} +
    + {{formGroup systemFields.tokenRingImg value=source.system.tokenRingImg localize=true}} +
    + {{formGroup systemFields.tokenSize.fields.height value=source.system.tokenSize.height localize=true placeholder=(localize "DAGGERHEART.ITEMS.Beastform.FIELDS.tokenSize.placeholder") }} {{formGroup systemFields.tokenSize.fields.width value=source.system.tokenSize.width localize=true placeholder=(localize "DAGGERHEART.ITEMS.Beastform.FIELDS.tokenSize.placeholder")}}
    From 059b814fdf853ce5a6ef09f2274e76fa0560bf4e Mon Sep 17 00:00:00 2001 From: Dapoulp <74197441+Dapoulp@users.noreply.github.com> Date: Tue, 8 Jul 2025 21:06:46 +0200 Subject: [PATCH 4/4] Feature/armor stack uses on damage (#300) * ArmorStack use as User query * Remove unnecessart args * Fixes --- daggerheart.mjs | 2 + .../dialogs/damageReductionDialog.mjs | 14 ++- module/applications/ui/chatLog.mjs | 4 +- module/documents/actor.mjs | 99 ++++++++++++------- module/systemRegistration/socket.mjs | 37 ++++--- templates/ui/chat/adversary-roll.hbs | 2 +- templates/ui/chat/duality-roll.hbs | 2 +- 7 files changed, 103 insertions(+), 57 deletions(-) diff --git a/daggerheart.mjs b/daggerheart.mjs index a6676f19..153c2f84 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -18,6 +18,7 @@ import { } from './module/systemRegistration/_module.mjs'; import { placeables } from './module/canvas/_module.mjs'; import { registerRollDiceHooks } from './module/dice/dhRoll.mjs'; +import { registerDHActorHooks } from './module/documents/actor.mjs'; Hooks.once('init', () => { CONFIG.DH = SYSTEM; @@ -154,6 +155,7 @@ Hooks.on('ready', () => { socketRegistration.registerSocketHooks(); registerCountdownApplicationHooks(); registerRollDiceHooks(); + registerDHActorHooks(); }); Hooks.once('dicesoniceready', () => {}); diff --git a/module/applications/dialogs/damageReductionDialog.mjs b/module/applications/dialogs/damageReductionDialog.mjs index 8a67ef98..3d33f2a3 100644 --- a/module/applications/dialogs/damageReductionDialog.mjs +++ b/module/applications/dialogs/damageReductionDialog.mjs @@ -1,6 +1,6 @@ import { damageKeyToNumber, getDamageLabel } from '../../helpers/utils.mjs'; -const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; +const { DialogV2, ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; export default class DamageReductionDialog extends HandlebarsApplicationMixin(ApplicationV2) { constructor(resolve, reject, actor, damage) { @@ -122,7 +122,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap getDamageInfo = () => { const selectedArmorMarks = Object.values(this.marks.armor).filter(x => x.selected); const selectedStressMarks = Object.values(this.marks.stress).filter(x => x.selected); - const stressReductions = Object.values(this.availableStressReductions).filter(red => red.selected); + const stressReductions = Object.values(this.availableStressReductions ?? {}).filter(red => red.selected); const currentMarks = this.actor.system.armor.system.marks.value + selectedArmorMarks.length + selectedStressMarks.length; @@ -210,9 +210,17 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap async close(fromSave) { if (!fromSave) { - this.reject(); + this.resolve(); } await super.close({}); } + + static async armorStackQuery({actorId, damage}) { + return new Promise(async (resolve, reject) => { + const actor = await fromUuid(actorId); + if(!actor || !actor?.isOwner) reject(); + new DamageReductionDialog(resolve, reject, actor, damage).render({ force: true }); + }) + } } diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index d17c26dc..0e2242d7 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -215,7 +215,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo if (message.system.onSave && message.system.targets.find(t => t.id === target.id)?.saved?.success === true) damage = Math.ceil(damage * (CONFIG.DH.ACTIONS.damageOnSave[message.system.onSave]?.mod ?? 1)); - await target.actor.takeDamage(damage, message.system.roll.type); + target.actor.takeDamage(damage, message.system.roll.type); } }; @@ -227,7 +227,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected')); for (var target of targets) { - await target.actor.takeHealing([{ value: message.system.roll.total, type: message.system.roll.type }]); + target.actor.takeHealing([{ value: message.system.roll.total, type: message.system.roll.type }]); } }; diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index b48d8c26..1fab0f71 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -1,10 +1,19 @@ import DamageSelectionDialog from '../applications/dialogs/damageSelectionDialog.mjs'; -import { GMUpdateEvent, socketEvent } from '../systemRegistration/socket.mjs'; +import { emitAsGM, emitAsOwner, GMUpdateEvent, socketEvent } from '../systemRegistration/socket.mjs'; import DamageReductionDialog from '../applications/dialogs/damageReductionDialog.mjs'; import { LevelOptionType } from '../data/levelTier.mjs'; import DHFeature from '../data/item/feature.mjs'; -export default class DhpActor extends foundry.documents.Actor { +export default class DhpActor extends Actor { + + /** + * Return the first Actor active owner. + */ + get owner() { + const user = this.hasPlayerOwner && game.users.players.find(u => this.testUserPermission(u, "OWNER") && u.active);; + if(!user) return game.user.isGM ? game.user : null; + return user; + } /** * Whether this actor is an NPC. @@ -440,49 +449,40 @@ export default class DhpActor extends foundry.documents.Actor { } async takeDamage(damage, type) { + if (Hooks.call(`${CONFIG.DH.id}.preTakeDamage`, this, damage, type) === false) return null; + if (this.type === 'companion') { await this.modifyResource([{ value: 1, type: 'stress' }]); return; } - const hpDamage = - damage >= this.system.damageThresholds.severe - ? 3 - : damage >= this.system.damageThresholds.major - ? 2 - : damage >= this.system.damageThresholds.minor - ? 1 - : 0; + const hpDamage = this.convertDamageToThreshold(damage); + + if (Hooks.call(`${CONFIG.DH.id}.postDamageTreshold`, this, hpDamage, damage, type) === false) return null; + + if(!hpDamage) return; + + const updates = [{ value: hpDamage, type: 'hitPoints' }]; if ( this.type === 'character' && this.system.armor && this.system.armor.system.marks.value < this.system.armorScore ) { - new Promise((resolve, reject) => { - new DamageReductionDialog(resolve, reject, this, hpDamage).render(true); - }) - .then(async ({ modifiedDamage, armorSpent, stressSpent }) => { - const resources = [ - { value: modifiedDamage, type: 'hitPoints' }, - ...(armorSpent ? [{ value: armorSpent, type: 'armorStack' }] : []), - ...(stressSpent ? [{ value: stressSpent, type: 'stress' }] : []) - ]; - await this.modifyResource(resources); - }) - .catch(() => { - const cls = getDocumentClass('ChatMessage'); - const msg = new cls({ - user: game.user.id, - content: game.i18n.format('DAGGERHEART.UI.Notifications.damageIgnore', { - character: this.name - }) - }); - cls.create(msg.toObject()); - }); - } else { - await this.modifyResource([{ value: hpDamage, type: 'hitPoints' }]); + const armorStackResult = await this.owner.query('armorStack', {actorId: this.uuid, damage: hpDamage}); + if(armorStackResult) { + const { modifiedDamage, armorSpent, stressSpent } = armorStackResult; + updates.find(u => u.type === 'hitPoints').value = modifiedDamage; + updates.push( + ...(armorSpent ? [{ value: armorSpent, type: 'armorStack' }] : []), + ...(stressSpent ? [{ value: stressSpent, type: 'stress' }] : []) + ); + } } + + await this.modifyResource(updates); + + if (Hooks.call(`${CONFIG.DH.id}.postTakeDamage`, this, damage, type) === false) return null; } async takeHealing(resources) { @@ -492,6 +492,8 @@ export default class DhpActor extends foundry.documents.Actor { async modifyResource(resources) { if (!resources.length) return; + + if(resources.find(r => r.type === 'stress')) this.convertStressDamageToHP(resources); let updates = { actor: { target: this, resources: {} }, armor: { target: this.system.armor, resources: {} } }; resources.forEach(r => { switch (r.type) { @@ -519,7 +521,8 @@ export default class DhpActor extends foundry.documents.Actor { }); Object.values(updates).forEach(async u => { if (Object.keys(u.resources).length > 0) { - if (game.user.isGM) { + await emitAsGM(GMUpdateEvent.UpdateDocument, u.target.update.bind(u.target), u.resources, u.target.uuid); + /* if (game.user.isGM) { await u.target.update(u.resources); } else { await game.socket.emit(`system.${CONFIG.DH.id}`, { @@ -530,8 +533,34 @@ export default class DhpActor extends foundry.documents.Actor { update: u.resources } }); - } + } */ } }); } + + convertDamageToThreshold(damage) { + return damage >= this.system.damageThresholds.severe + ? 3 + : damage >= this.system.damageThresholds.major + ? 2 + : damage >= this.system.damageThresholds.minor + ? 1 + : 0; + } + + convertStressDamageToHP(resources) { + const stressDamage = resources.find(r => r.type === 'stress'), + newValue = this.system.resources.stress.value + stressDamage.value; + if(newValue <= this.system.resources.stress.maxTotal) return; + const hpDamage = resources.find(r => r.type === 'hitPoints'); + if(hpDamage) hpDamage.value++; + else resources.push({ + type: 'hitPoints', + value: 1 + }) + } } + +export const registerDHActorHooks = () => { + CONFIG.queries.armorStack = DamageReductionDialog.armorStackQuery; +} \ No newline at end of file diff --git a/module/systemRegistration/socket.mjs b/module/systemRegistration/socket.mjs index d7f79df9..b336a012 100644 --- a/module/systemRegistration/socket.mjs +++ b/module/systemRegistration/socket.mjs @@ -63,21 +63,28 @@ export const registerSocketHooks = () => { }); }; -export const emitAsGM = async (eventName, callback, args) => { +export const emitAsGM = async (eventName, callback, update, uuid = null) => { if(!game.user.isGM) { - return new Promise(async (resolve, reject) => { - try { - const response = await game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.GMUpdate, - data: { - action: eventName, - update: args - } - }); - resolve(response); - } catch (error) { - reject(error); + return await game.socket.emit(`system.${CONFIG.DH.id}`, { + action: socketEvent.GMUpdate, + data: { + action: eventName, + uuid, + update } - }) - } else return callback(args); + }); + } else return callback(update); } + +export const emitAsOwner = (eventName, userId, args) => { + if(userId === game.user.id) return; + if(!eventName || !userId) return false; + game.socket.emit(`system.${CONFIG.DH.id}`, { + action: eventName, + data: { + userId, + ...args + } + }); + return false; +} \ No newline at end of file diff --git a/templates/ui/chat/adversary-roll.hbs b/templates/ui/chat/adversary-roll.hbs index 1f97fefe..a8918062 100644 --- a/templates/ui/chat/adversary-roll.hbs +++ b/templates/ui/chat/adversary-roll.hbs @@ -49,7 +49,7 @@
    {{#if damage.roll}}
    - +
    {{else}}
    diff --git a/templates/ui/chat/duality-roll.hbs b/templates/ui/chat/duality-roll.hbs index 4242c515..66d32e95 100644 --- a/templates/ui/chat/duality-roll.hbs +++ b/templates/ui/chat/duality-roll.hbs @@ -134,7 +134,7 @@
    {{#if hasDamage}} {{#if damage.roll}} - + {{else}} {{/if}}