From 79d652261459eeed358ff597154e36c7e8fa9cd6 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Tue, 14 Jul 2026 08:39:53 -0400 Subject: [PATCH 1/8] [Feature] Add support for GM Notes (#2082) * Add support for GM Notes * Localize GM Notes header label * Fix active editor height and menu auto sizing * Add tooltip to add gm note button --- daggerheart.mjs | 4 + lang/en.json | 6 ++ module/applications/sheets/api/base-item.mjs | 52 ++++++++++++- module/data/item/base.mjs | 23 ++++-- styles/less/global/elements.less | 36 ++++++++- styles/less/global/feature-section.less | 2 +- styles/less/global/global.less | 4 + styles/less/global/item-header.less | 35 +++++---- styles/less/global/tab-description.less | 77 ++++++++++++++++++- .../sheets/actors/actor-sheet-shared.less | 4 - styles/less/sheets/items/beastform.less | 9 +++ styles/less/sheets/items/feature.less | 10 +-- styles/less/sheets/items/index.less | 4 +- .../less/sheets/items/item-sheet-shared.less | 20 ++++- styles/less/utils/mixin.less | 2 +- system.json | 20 ++--- .../sheets/global/tabs/tab-description.hbs | 34 +++++--- 17 files changed, 278 insertions(+), 64 deletions(-) diff --git a/daggerheart.mjs b/daggerheart.mjs index 63127aa4..f91eedbe 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -267,6 +267,10 @@ Hooks.on('i18nInit', () => { }); Hooks.on('setup', () => { + if (game.user.isGM) { + document.body.dataset.gm = true; + } + CONFIG.statusEffects = [ ...CONFIG.statusEffects.filter(x => !['dead', 'unconscious'].includes(x.id)), ...Object.values(SYSTEM.GENERAL.conditions()).map(x => ({ diff --git a/lang/en.json b/lang/en.json index 99b6baeb..508a771d 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2550,6 +2550,9 @@ }, "identifier": { "label": "Identifier" + }, + "gmNotes": { + "label": "GM Notes" } }, "Ancestry": { @@ -2565,6 +2568,9 @@ "severe": "Severe Threshold" } }, + "Base": { + "addGMNote": "Add GM Note" + }, "Beastform": { "FIELDS": { "beastformType": { "label": "Beastform Type" }, diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs index 1e08fc05..70a6bcc6 100644 --- a/module/applications/sheets/api/base-item.mjs +++ b/module/applications/sheets/api/base-item.mjs @@ -30,7 +30,8 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { addFeature: DHBaseItemSheet.#addFeature, deleteFeature: DHBaseItemSheet.#deleteFeature, addResource: DHBaseItemSheet.#addResource, - removeResource: DHBaseItemSheet.#removeResource + removeResource: DHBaseItemSheet.#removeResource, + editGMNote: DHBaseItemSheet.#onEditGMNote }, dragDrop: [ { dragSelector: null, dropSelector: '.drop-section' }, @@ -76,10 +77,16 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { /**@inheritdoc */ async _preparePartContext(partId, context, options) { await super._preparePartContext(partId, context, options); + const TextEditor = foundry.applications.ux.TextEditor.implementation; switch (partId) { case 'description': - context.enrichedDescription = await this.document.system.getEnrichedDescription(); + context.enrichedDescription = await this.document.system.getEnrichedDescription({ gmNotes: false }); + context.enrichedGMNotes = await TextEditor.implementation.enrichHTML(this.item.system.gmNotes, { + relativeTo: this.item, + rollData: this.item.getRollData(), + secrets: this.item.isOwner + }) break; case 'effects': await this._prepareEffectsContext(context, options); @@ -331,4 +338,45 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { } } } + + /** + * Handles the Add GM Note button being pressed. This is only used when an item has no GM notes. + * Later edits to a GM note instead go through the normal editor toggle workflow. + * @this DHBaseItemSheet + */ + static #onEditGMNote() { + // Open the editor, which might be hidden. We remove the css class to hide temporarily + // so that menu auto resizing functions properly. + const editor = this.element.querySelector('prose-mirror[name="system.gmNotes"]'); + const wasHidden = editor.classList.contains('hide-if-inactive'); + editor.classList.remove('hide-if-inactive'); + editor.open = true; + window.setTimeout(() => { + if (wasHidden) editor.classList.add('hide-if-inactive'); + }, 0); + } + + /** @inheritdoc */ + async _onRender(context, options) { + await super._onRender(context, options); + + // Render an add gmnotes button if there are no set GM notes. + // We need to re-render on close since its possible to prosemirror to close *without* triggering a full re-render + if (game.user.isGM && !this.item.system.gmNotes) { + const description = this.element.querySelector('[name="system.description"]'); + const addButton = () => { + if (description.querySelector('[data-action=editGMNote]')) return; + + const button = document.createElement('button'); + button.type = 'button'; + button.classList.add('icon', 'toggle', 'fa-regular', 'fa-note-medical'); + button.dataset.action = 'editGMNote'; + button.dataset.tooltip = 'DAGGERHEART.ITEMS.Base.addGMNote'; + description.appendChild(button); + } + + addButton(); + description.addEventListener('close', () => addButton()); + } + } } diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 131ef10f..095ba8f2 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -49,7 +49,10 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { }) }; - if (this.metadata.hasDescription) schema.description = new fields.HTMLField({ required: true, nullable: true }); + if (this.metadata.hasDescription) { + schema.description = new fields.HTMLField({ required: true, nullable: true }); + schema.gmNotes = new fields.HTMLField({ required: true, nullable: true }); + } if (this.metadata.hasResource) { schema.resource = new fields.SchemaField( @@ -134,7 +137,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { /** * Augments the description for the item with type specific info to display. Implemented in applicable item subtypes. * @param {object} [options] - Options that modify the styling of the rendered template. { headerStyle: undefined|'none'|'large' } - * @returns {string} + * @returns {Promise<{ prefix: string | null; value: string | null; suffix: string | null }>} */ async getDescriptionData(_options) { return { prefix: null, value: this.description, suffix: null }; @@ -145,14 +148,24 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { * @param {object} [options] - Options that modify the styling of the rendered template. { headerStyle: undefined|'none'|'large' } * @returns {Promise} */ - async getEnrichedDescription() { + async getEnrichedDescription({ gmNotes = true } = {}) { if (!this.metadata.hasDescription) return ''; const { prefix, value, suffix } = await this.getDescriptionData(); - const fullDescription = [prefix, value, suffix].filter(p => !!p).join('\n
\n'); + let fullDescription = [prefix, value, suffix].filter(p => !!p).join('\n
\n'); + if (this.gmNotes && gmNotes) { + const gmNotesElement = document.createElement('section'); + gmNotesElement.classList.add('gm-notes-section'); + gmNotesElement.dataset.visibility = 'gm'; + const header = document.createElement('header'); + header.classList.add('gm-notes'); + header.textContent = _loc('DAGGERHEART.ITEMS.FIELDS.gmNotes.label'); + gmNotesElement.innerHTML = header.outerHTML + this.gmNotes; + fullDescription += gmNotesElement.outerHTML; + } return await foundry.applications.ux.TextEditor.implementation.enrichHTML(fullDescription, { - relativeTo: this, + relativeTo: this.parent, rollData: this.getRollData(), secrets: this.parent.isOwner }); diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index 1b7ed072..d31e09b9 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -595,7 +595,37 @@ margin-top: 4px; color: light-dark(#14142599, #efe6d850); font-size: var(--font-size-12); - padding-left: 3px; + padding-left: 16px; + } + + section.gm-notes-section { + padding-bottom: var(--spacer-4); + header.gm-notes + p { + margin-top: 0; + } + } + + header.gm-notes { + position: relative; + display: flex; + gap: 6px; + align-items: center; + &::before, + &::after { + content: " "; + flex: 1; + border-bottom: 1px solid var(--color-dark-6); + } + &::before { + mask-image: linear-gradient(270deg, black 0%, black calc(100% - 10px), transparent 100%); + } + &::after { + mask-image: linear-gradient(270deg, transparent 0%, black 10px, black 100%); + } + margin-top: var(--spacer-8); + margin-bottom: var(--spacer-4); + font-size: var(--font-size-11); + text-transform: uppercase; } secret-block { @@ -866,4 +896,8 @@ right: 2px; } } + + .gm-notes { + font-style: italic; + } } diff --git a/styles/less/global/feature-section.less b/styles/less/global/feature-section.less index 2fd4e20f..ecfb4ff6 100644 --- a/styles/less/global/feature-section.less +++ b/styles/less/global/feature-section.less @@ -3,7 +3,7 @@ .sheet.daggerheart.dh-style.item { .tab.features { - padding: 0 10px; + padding: 7px 10px; overflow-y: auto; .feature-list { display: flex; diff --git a/styles/less/global/global.less b/styles/less/global/global.less index 19a9e519..2c44c94e 100644 --- a/styles/less/global/global.less +++ b/styles/less/global/global.less @@ -111,3 +111,7 @@ body.theme-light, .themed.theme-light { color-scheme: light; } + +body:not([data-gm=true]) [data-visibility="gm"] { + display: none; +} \ No newline at end of file diff --git a/styles/less/global/item-header.less b/styles/less/global/item-header.less index f47ca7dc..1a8d7fce 100755 --- a/styles/less/global/item-header.less +++ b/styles/less/global/item-header.less @@ -12,12 +12,14 @@ }); .application.sheet.daggerheart.dh-style { + --portrait-size: 150px; + .item-sheet-header { display: flex; .profile { - height: 150px; - width: 150px; + height: var(--portrait-size); + width: var(--portrait-size); object-fit: cover; border-right: 1px solid light-dark(@dark-blue, @golden); border-bottom: 1px solid light-dark(@dark-blue, @golden); @@ -34,19 +36,24 @@ text-align: center; width: 80%; - .item-name input[type='text'] { - font-size: var(--font-size-32); - height: 42px; - text-align: center; - width: 90%; - transition: all 0.3s ease; - outline: 2px solid transparent; - border: 1px solid transparent; + .item-name { + display: flex; + flex-direction: column; + margin: 10px 10px 0 10px; + input[type='text'] { + font-size: var(--font-size-30); + text-align: center; + width: 100%; + transition: all 0.3s ease; + outline: 2px solid transparent; + border: 1px solid transparent; + text-overflow: ellipsis; - &:hover[type='text'], - &:focus[type='text'] { - box-shadow: none; - outline: 2px solid light-dark(@dark-blue, @golden); + &:hover[type='text'], + &:focus[type='text'] { + box-shadow: none; + outline: 2px solid light-dark(@dark-blue, @golden); + } } } diff --git a/styles/less/global/tab-description.less b/styles/less/global/tab-description.less index 5c18e02b..e2869723 100644 --- a/styles/less/global/tab-description.less +++ b/styles/less/global/tab-description.less @@ -6,11 +6,80 @@ display: flex; flex-direction: column; flex: 1; - overflow-y: hidden !important; - padding-top: 10px; + overflow: hidden; + padding: 0; + margin: 0; - prose-mirror.active + .artist-attribution { - display: none; + .description-section { + flex: 1; + display: flex; + flex-direction: column; + overflow: auto; + padding: 12px 16px 4px 16px; + .with-scroll-shadows(); + prose-mirror { + button.toggle { + top: 0px; + right: 0; + } + button[data-action=editGMNote] { + right: calc(var(--button-size) + 4px); + } + &.inactive { + height: unset!important; + overflow: unset; + .editor-content { + position: relative; + overflow: unset; + + // Allows content links to peek out + margin-top: -4px; + padding: 4px 0 0 0; + } + } + &.active { + --min-height: 250px; + padding: 8px 0 0 16px; + button[data-action=editGMNote] { + display: none; + } + .editor-content { + padding-right: 16px; + padding-bottom: 4px; + } + } + } + /** Hide editors that are empty when inactive if we need them to be */ + prose-mirror.inactive.hide-if-inactive { + display: none; + } + &:has(prose-mirror.active) { + padding: 0; + } + /** Description should fill available room (with overriden exceptions) */ + prose-mirror[name="system.description"] { + flex: 1 0; + } + &:has(prose-mirror[name="system.gmNotes"]:not(.hide-if-inactive)) { + prose-mirror.inactive { + --min-height: 3rem; + &[name="system.description"] { + flex: 0 0; + } + &[name="system.gmNotes"] { + flex: 1 0; + } + } + } + } + + /** Hide other elements if an editor is open */ + &:has(prose-mirror.active) { + prose-mirror.inactive, + header.gm-notes, + .artist-attribution { + display: none; + } } } } diff --git a/styles/less/sheets/actors/actor-sheet-shared.less b/styles/less/sheets/actors/actor-sheet-shared.less index 3e233013..a464d7a1 100644 --- a/styles/less/sheets/actors/actor-sheet-shared.less +++ b/styles/less/sheets/actors/actor-sheet-shared.less @@ -93,10 +93,6 @@ padding: 8px 0 0 16px; } } - - .artist-attribution { - padding-left: 16px; - } } .search-section { diff --git a/styles/less/sheets/items/beastform.less b/styles/less/sheets/items/beastform.less index 100b024a..017c4ef0 100644 --- a/styles/less/sheets/items/beastform.less +++ b/styles/less/sheets/items/beastform.less @@ -1,4 +1,6 @@ .application.sheet.daggerheart.dh-style.beastform { + --portrait-size: 130px; + .settings.tab { .advantage-on-section { display: flex; @@ -9,4 +11,11 @@ font-style: italic; } } + .tab.features.active { + display: flex; + flex-direction: column; + gap: 10px; + padding: 8px calc(12px - var(--scrollbar-width)) 4px 12px; + .stable-scroll-container(); + } } diff --git a/styles/less/sheets/items/feature.less b/styles/less/sheets/items/feature.less index f3c7cd49..9166fac1 100644 --- a/styles/less/sheets/items/feature.less +++ b/styles/less/sheets/items/feature.less @@ -2,17 +2,9 @@ @import '../../utils/fonts.less'; .application.sheet.daggerheart.dh-style.feature { - .item-sheet-header { - display: flex; - - .profile { - height: 130px; - width: 130px; - } - } + --portrait-size: 130px; section.tab { - height: 400px; overflow-y: auto; } } diff --git a/styles/less/sheets/items/index.less b/styles/less/sheets/items/index.less index 7c40a2e3..7f9bb684 100644 --- a/styles/less/sheets/items/index.less +++ b/styles/less/sheets/items/index.less @@ -1,6 +1,6 @@ +@import './item-sheet-shared.less'; @import './beastform.less'; @import './class.less'; @import './domain-card.less'; @import './feature.less'; -@import './heritage.less'; -@import './item-sheet-shared.less'; \ No newline at end of file +@import './heritage.less'; \ No newline at end of file diff --git a/styles/less/sheets/items/item-sheet-shared.less b/styles/less/sheets/items/item-sheet-shared.less index 5155ad70..63846b8e 100644 --- a/styles/less/sheets/items/item-sheet-shared.less +++ b/styles/less/sheets/items/item-sheet-shared.less @@ -1,4 +1,4 @@ -.application.sheet.daggerheart.dh-style.item { +.item.daggerheart.dh-style:where(.application.sheet) { &.minimized { .attribution-header-label { display: none; @@ -14,4 +14,22 @@ button.plain.inline-control { flex: 0 0 auto; } + + .tab-navigation { + margin-bottom: 0; + } + + /** Default tab stylings */ + .tab.active { + padding-top: 8px; + .with-scroll-shadows(); + + &.effects { + display: flex; + flex-direction: column; + gap: 10px; + padding: 8px calc(12px - var(--scrollbar-width)) 4px 12px; + .stable-scroll-container(); + } + } } diff --git a/styles/less/utils/mixin.less b/styles/less/utils/mixin.less index fb70d0a3..429fb3ef 100644 --- a/styles/less/utils/mixin.less +++ b/styles/less/utils/mixin.less @@ -226,7 +226,7 @@ ul, ol { - margin: 1rem 0; + margin: 0.5rem 0; padding: 0 0 0 1.25rem; li { diff --git a/system.json b/system.json index 93b1dea7..214ab2ee 100644 --- a/system.json +++ b/system.json @@ -256,34 +256,34 @@ }, "Item": { "ancestry": { - "htmlFields": ["description"] + "htmlFields": ["description", "gmNotes"] }, "community": { - "htmlFields": ["description"] + "htmlFields": ["description", "gmNotes"] }, "class": { - "htmlFields": ["description"] + "htmlFields": ["description", "gmNotes"] }, "subclass": { - "htmlFields": ["description"] + "htmlFields": ["description", "gmNotes"] }, "feature": { - "htmlFields": ["description"] + "htmlFields": ["description", "gmNotes"] }, "domainCard": { - "htmlFields": ["description"] + "htmlFields": ["description", "gmNotes"] }, "loot": { - "htmlFields": ["description"] + "htmlFields": ["description", "gmNotes"] }, "consumable": { - "htmlFields": ["description"] + "htmlFields": ["description", "gmNotes"] }, "weapon": { - "htmlFields": ["description"] + "htmlFields": ["description", "gmNotes"] }, "armor": { - "htmlFields": ["description"] + "htmlFields": ["description", "gmNotes"] }, "beastform": {} }, diff --git a/templates/sheets/global/tabs/tab-description.hbs b/templates/sheets/global/tabs/tab-description.hbs index 71995a51..3fdf1a93 100755 --- a/templates/sheets/global/tabs/tab-description.hbs +++ b/templates/sheets/global/tabs/tab-description.hbs @@ -1,11 +1,25 @@ -
- {{formInput systemFields.description value=document.system.description enriched=enrichedDescription toggled=true}} - - {{#if (and showAttribution document.system.attribution.artist)}} - - {{/if}} +
+
+ {{formInput systemFields.description value=document.system.description enriched=enrichedDescription toggled=true}} + {{#if (and systemFields.gmNotes @root.user.isGM)}} +
+ {{#if enrichedGMNotes}} +
{{localize "DAGGERHEART.ITEMS.FIELDS.gmNotes.label"}}
+ {{/if}} + {{{enrichedGMNotes}}} +
+ {{/if}} +
+ {{#if (and showAttribution document.system.attribution.artist)}} + + {{/if}}
\ No newline at end of file From d76b4bb707fbfad3412c3a14882eeb7f4c4bcf64 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Sat, 18 Jul 2026 05:42:50 -0400 Subject: [PATCH 2/8] Fix chat being jumpy as timestamps get longer (#2095) --- styles/less/global/chat.less | 87 +++++++++++++++++++----------- templates/ui/chat/chat-message.hbs | 41 +++++++------- 2 files changed, 79 insertions(+), 49 deletions(-) diff --git a/styles/less/global/chat.less b/styles/less/global/chat.less index b9478ea4..4e29cfff 100644 --- a/styles/less/global/chat.less +++ b/styles/less/global/chat.less @@ -7,12 +7,12 @@ .chat-log .chat-message { background-image: url('../assets/parchments/dh-parchment-light.png'); - .message-header .message-header-metadata .message-metadata, - .message-header .message-header-main .message-sub-header-container { + .message-header .message-header-main .message-metadata, + .message-header .message-header-main .name { color: @dark; } - .message-header .message-header-main .message-sub-header-container h4 { + .message-header .message-header-main h4 { color: @dark-blue; } @@ -42,27 +42,12 @@ .message-header { display: flex; - gap: 4px; + gap: 8px; padding: 8px; + align-items: center; - .message-header-metadata { - flex: none; - display: flex; - flex-direction: column; - - .message-metadata { - font-family: @font-body; - color: @beige; - } - } - - .message-header-main { - display: flex; - align-items: center; - gap: 8px; - flex: 1; - overflow: hidden; - + .portrait { + flex: 0 0 auto; .actor-img { border-radius: 50%; width: 40px; @@ -70,20 +55,60 @@ object-fit: cover; object-position: top center; } + } - .message-sub-header-container { + .message-header-main { + display: flex; + align-items: center; + column-gap: 4px; + row-gap: 2px; + flex: 1; + overflow: hidden; + + display: grid; + grid-template: + "title metadata" + "subtitle subtitle"; + + h4 { + font-size: var(--font-size-16); + font-weight: bold; + margin-bottom: 0; + font-family: @font-subtitle; + color: @golden; + white-space: nowrap; + text-overflow: ellipsis; + flex: 1; + overflow: hidden; + align-self: flex-end; + } + + .message-metadata { + font-family: @font-body; + color: @beige; + white-space: nowrap; + align-items: baseline; + .message-timestamp { + font-size: var(--font-size-11); + } + } + + .subtitle { + grid-area: subtitle; flex: 1; display: flex; - flex-direction: column; justify-content: space-between; color: @beige; - - h4 { - font-size: var(--font-size-16); - font-weight: bold; - margin-bottom: 0; - font-family: @font-subtitle; - color: @golden; + gap: 4px; + align-items: baseline; + line-height: 1; + .name { + flex: 1; + } + .whisper-to { + color: @color-text-subtle; + flex: 0; + white-space: nowrap; } } } diff --git a/templates/ui/chat/chat-message.hbs b/templates/ui/chat/chat-message.hbs index 87ecce39..92490cec 100644 --- a/templates/ui/chat/chat-message.hbs +++ b/templates/ui/chat/chat-message.hbs @@ -1,23 +1,18 @@
  • -
    +
    -
    - {{#if message.title}} -

    {{message.title}}

    -
    {{alias}} {{#if author.isGM}}(GM){{/if}}
    - {{else}} - {{#unless actor.name}} -

    {{author.name}}

    - {{else}} -

    {{alias}}

    -
    {{author.name}}
    - {{/unless}} - {{/if}} -
    -
    From c181a47cc2b5ff421ff0758fc450e3274fbb4f1e Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Sat, 18 Jul 2026 12:28:42 -0400 Subject: [PATCH 3/8] Swap item macro tooltip with system item tooltips (#2092) --- module/documents/tooltipManager.mjs | 369 +++++++++++++++------------- 1 file changed, 202 insertions(+), 167 deletions(-) diff --git a/module/documents/tooltipManager.mjs b/module/documents/tooltipManager.mjs index 3e3f4a16..6e794b6a 100644 --- a/module/documents/tooltipManager.mjs +++ b/module/documents/tooltipManager.mjs @@ -4,199 +4,234 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti #wide = false; #bordered = false; + /** @inheritdoc */ async activate(element, options = {}) { - const { TextEditor } = foundry.applications.ux; + this.#wide = false; + this.#bordered = false; - let html = options.html; - if (element.dataset.tooltip?.startsWith('#battlepoints#')) { - this.#wide = true; - this.#bordered = true; - - html = await this.getBattlepointHTML(element.dataset.combatId); - options.direction = this._determineItemTooltipDirection(element); - super.activate(element, { ...options, html: html }); - - const lockedTooltip = this.lockTooltip(); - lockedTooltip.querySelectorAll('.battlepoint-toggle-container input').forEach(element => { - element.addEventListener('input', this.toggleModifier.bind(this)); - }); - return; - } else { - this.#wide = false; - this.#bordered = false; + const isMacro = document.getElementById('action-bar').contains(element); + const macro = isMacro ? game.macros.get(game.user.hotbar[Number(element.dataset.slot)] ?? null) : null; + const macroItemUuid = macro?.type === 'script' ? macro.command.match(/await game\.system\.api\.applications\.ui\.DhHotbar\.useItem\("([^"]+)"\);/)?.[1] : null; + if (macroItemUuid && await fromUuid(macroItemUuid, { strict: false })) { + element.dataset.tooltip = `#item#${macroItemUuid}`; + options.direction = this.constructor.TOOLTIP_DIRECTIONS.UP; } - if (element.dataset.tooltip === '#effect-display#') { - this.#bordered = true; - let effect = {}; - if (element.dataset.uuid) { - const effectItem = await foundry.utils.fromUuid(element.dataset.uuid); - const effectData = effectItem.toObject(); + let html = options.html; + const key = element.dataset.tooltip?.match(/^#([\w-]+)#/)?.[1]; + switch (key) { + case 'battlepoints': + return this.#activateBattlepoints(element, options); + case 'effect-display': + html = await this.#activateEffectDisplay(element, options); + break; + case 'item': + html = await this.#activateItem(element, options); + break; + case 'attack': + html = await this.#activateAttack(element, options); + break; + case 'shortRest': + case 'longRest': + html = await this.#activateRest(element, options); + break; + case 'advantage': + case 'disadvantage': + html = await this.#activateAdvantageDisadvantage(element, options); + break; + case 'deathMove': + html = await this.#activateDeathMove(element, options); + break; + } - effect = { - ...effectData, - name: game.i18n.localize(effectData.name) + this.noOffset = options.noOffset; + super.activate(element, { ...options, html }); + } + + async #activateBattlepoints(element, options) { + this.#wide = true; + this.#bordered = true; + + const html = await this.getBattlepointHTML(element.dataset.combatId); + options.direction = this._determineItemTooltipDirection(element); + super.activate(element, { ...options, html: html }); + + const lockedTooltip = this.lockTooltip(); + lockedTooltip.querySelectorAll('.battlepoint-toggle-container input').forEach(element => { + element.addEventListener('input', this.toggleModifier.bind(this)); + }); + } + + async #activateEffectDisplay(element, options) { + this.#bordered = true; + let effect = {}; + if (element.dataset.uuid) { + const effectItem = await foundry.utils.fromUuid(element.dataset.uuid); + const effectData = effectItem.toObject(); + + effect = { + ...effectData, + name: game.i18n.localize(effectData.name) + }; + + if (effectData.type === 'beastform') { + const beastformData = { + features: [], + advantageOn: effectData.system.advantageOn, + beastformAttackData: game.system.api.data.items.DHBeastform.getBeastformAttackData(effectItem) }; - if (effectData.type === 'beastform') { - const beastformData = { - features: [], - advantageOn: effectData.system.advantageOn, - beastformAttackData: game.system.api.data.items.DHBeastform.getBeastformAttackData(effectItem) - }; - - const features = effectItem.parent.items.filter(x => effectItem.system.featureIds.includes(x.id)); - for (const feature of features) { - const featureData = feature.toObject(); - featureData.enrichedDescription = await feature.system.getEnrichedDescription(); - beastformData.features.push(featureData); - } - - effect.description = await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/ui/tooltip/parts/beastformData.hbs', - { - item: { system: beastformData } - } - ); - } else { - effect.description = game.i18n.localize( - effectData.description ?? effectData.parent.system.description - ); + const features = effectItem.parent.items.filter(x => effectItem.system.featureIds.includes(x.id)); + for (const feature of features) { + const featureData = feature.toObject(); + featureData.enrichedDescription = await feature.system.getEnrichedDescription(); + beastformData.features.push(featureData); } - } else { - const conditions = CONFIG.DH.GENERAL.conditions(); - const condition = conditions[element.dataset.condition]; - effect = { - ...condition, - name: game.i18n.localize(condition.name), - description: game.i18n.localize(condition.description), - appliedBy: element.dataset.appliedBy, - isLockedCondition: true - }; - } - html = await foundry.applications.handlebars.renderTemplate( - `systems/daggerheart/templates/ui/tooltip/effect-display.hbs`, + effect.description = await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/ui/tooltip/parts/beastformData.hbs', + { + item: { system: beastformData } + } + ); + } else { + effect.description = game.i18n.localize( + effectData.description ?? effectData.parent.system.description + ); + } + } else { + const conditions = CONFIG.DH.GENERAL.conditions(); + const condition = conditions[element.dataset.condition]; + effect = { + ...condition, + name: game.i18n.localize(condition.name), + description: game.i18n.localize(condition.description), + appliedBy: element.dataset.appliedBy, + isLockedCondition: true + }; + } + + const html = await foundry.applications.handlebars.renderTemplate( + `systems/daggerheart/templates/ui/tooltip/effect-display.hbs`, + { + effect + } + ); + + this.tooltip.innerHTML = html; + options.direction = this._determineItemTooltipDirection(element); + + return html; + } + + async #activateItem(element, options) { + const itemUuid = element.dataset.tooltip.slice(6); + const item = await foundry.utils.fromUuid(itemUuid); + if (item) { + const isAction = item instanceof game.system.api.models.actions.actionsTypes.base; + const isEffect = item instanceof ActiveEffect; + await this.enrichText(item); + + const type = isAction ? 'action' : isEffect ? 'effect' : item.type; + const html = await foundry.applications.handlebars.renderTemplate( + `systems/daggerheart/templates/ui/tooltip/${type}.hbs`, { - effect + item: item, + description: item.system?.enrichedDescription ?? item.enrichedDescription, + config: CONFIG.DH, + allDomains: CONFIG.DH.DOMAIN.allDomains() } ); this.tooltip.innerHTML = html; - options.direction = this._determineItemTooltipDirection(element); - } else { - this.#bordered = false; + options.direction ??= this._determineItemTooltipDirection(element); + return html; } - if (element.dataset.tooltip?.startsWith('#item#')) { - const itemUuid = element.dataset.tooltip.slice(6); - const item = await foundry.utils.fromUuid(itemUuid); - if (item) { - const isAction = item instanceof game.system.api.models.actions.actionsTypes.base; - const isEffect = item instanceof ActiveEffect; - await this.enrichText(item); + return null; + } - const type = isAction ? 'action' : isEffect ? 'effect' : item.type; - html = await foundry.applications.handlebars.renderTemplate( - `systems/daggerheart/templates/ui/tooltip/${type}.hbs`, - { - item: item, - description: item.system?.enrichedDescription ?? item.enrichedDescription, - config: CONFIG.DH, - allDomains: CONFIG.DH.DOMAIN.allDomains() - } - ); + async #activateAttack(element, options) { + const actorUuid = element.dataset.tooltip.slice(8); + const actor = await foundry.utils.fromUuid(actorUuid); + const attack = actor.system.attack; - this.tooltip.innerHTML = html; - options.direction = this._determineItemTooltipDirection(element); + const description = await foundry.applications.ux.TextEditor.enrichHTML(attack.description); + const html = await foundry.applications.handlebars.renderTemplate( + `systems/daggerheart/templates/ui/tooltip/attack.hbs`, + { + attack: attack, + description: description, + parent: actor, + config: CONFIG.DH } - } else { - const attack = element.dataset.tooltip?.startsWith('#attack#'); - if (attack) { - const actorUuid = element.dataset.tooltip.slice(8); - const actor = await foundry.utils.fromUuid(actorUuid); - const attack = actor.system.attack; + ); - const description = await TextEditor.enrichHTML(attack.description); - html = await foundry.applications.handlebars.renderTemplate( - `systems/daggerheart/templates/ui/tooltip/attack.hbs`, - { - attack: attack, - description: description, - parent: actor, - config: CONFIG.DH - } - ); + this.tooltip.innerHTML = html; + return html; + } - this.tooltip.innerHTML = html; - } + async #activateAdvantageDisadvantage(element, options) { + const isAdvantage = element.dataset.tooltip?.startsWith('#advantage#'); + const actorUuid = element.dataset.tooltip.slice(isAdvantage ? 11 : 14); + const actor = await foundry.utils.fromUuid(actorUuid); - const shortRest = element.dataset.tooltip?.startsWith('#shortRest#'); - const longRest = element.dataset.tooltip?.startsWith('#longRest#'); - if (shortRest || longRest) { - const key = element.dataset.tooltip.slice(shortRest ? 11 : 10); - - const moves = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).restMoves[ - element.dataset.restType - ].moves; - const move = moves[key]; - const description = await TextEditor.enrichHTML(move.description); - html = await foundry.applications.handlebars.renderTemplate( - `systems/daggerheart/templates/ui/tooltip/downtime.hbs`, - { - move: move, - description: description - } - ); - - this.tooltip.innerHTML = html; - options.direction = this._determineItemTooltipDirection( - element, - this.constructor.TOOLTIP_DIRECTIONS.RIGHT - ); - } - - const isAdvantage = element.dataset.tooltip?.startsWith('#advantage#'); - const isDisadvantage = element.dataset.tooltip?.startsWith('#disadvantage#'); - if (isAdvantage || isDisadvantage) { - const actorUuid = element.dataset.tooltip.slice(isAdvantage ? 11 : 14); - const actor = await foundry.utils.fromUuid(actorUuid); - - if (actor) { - html = await foundry.applications.handlebars.renderTemplate( - `systems/daggerheart/templates/ui/tooltip/advantage.hbs`, - { - sources: isAdvantage ? actor.system.advantageSources : actor.system.disadvantageSources - } - ); - - this.tooltip.innerHTML = html; + if (actor) { + const html = await foundry.applications.handlebars.renderTemplate( + `systems/daggerheart/templates/ui/tooltip/advantage.hbs`, + { + sources: isAdvantage ? actor.system.advantageSources : actor.system.disadvantageSources } - } + ); - const deathMove = element.dataset.tooltip?.startsWith('#deathMove#'); - if (deathMove) { - const name = element.dataset.deathName; - const img = element.dataset.deathImg; - const description = element.dataset.deathDescription; - - html = await foundry.applications.handlebars.renderTemplate( - `systems/daggerheart/templates/ui/tooltip/death-move.hbs`, - { - move: { name: name, img: img, description: description } - } - ); - - this.tooltip.innerHTML = html; - options.direction = this._determineItemTooltipDirection( - element, - this.constructor.TOOLTIP_DIRECTIONS.RIGHT - ); - } + this.tooltip.innerHTML = html; + return html; } + return null; + } - this.noOffset = options.noOffset; - super.activate(element, { ...options, html: html }); + async #activateDeathMove(element, options) { + const name = element.dataset.deathName; + const img = element.dataset.deathImg; + const description = element.dataset.deathDescription; + + const html = await foundry.applications.handlebars.renderTemplate( + `systems/daggerheart/templates/ui/tooltip/death-move.hbs`, + { + move: { name: name, img: img, description: description } + } + ); + + this.tooltip.innerHTML = html; + options.direction = this._determineItemTooltipDirection( + element, + this.constructor.TOOLTIP_DIRECTIONS.RIGHT + ); + return html; + } + + async #activateRest(element, options) { + const isShortRest = element.dataset.tooltip?.startsWith('#shortRest#'); + const key = element.dataset.tooltip.slice(isShortRest ? 11 : 10); + const moves = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).restMoves[ + element.dataset.restType + ].moves; + const move = moves[key]; + const description = await foundry.applications.ux.TextEditor.enrichHTML(move.description); + const html = await foundry.applications.handlebars.renderTemplate( + `systems/daggerheart/templates/ui/tooltip/downtime.hbs`, + { + move: move, + description: description + } + ); + + this.tooltip.innerHTML = html; + options.direction = this._determineItemTooltipDirection( + element, + this.constructor.TOOLTIP_DIRECTIONS.RIGHT + ); + return html; } _setAnchor(direction) { From 4b651836ffb2ee6436258b6e12e33bf26b831de3 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Sat, 18 Jul 2026 12:33:19 -0400 Subject: [PATCH 4/8] Start supporting flat damage without custom formulas (#2077) --- module/data/fields/action/damageField.mjs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/module/data/fields/action/damageField.mjs b/module/data/fields/action/damageField.mjs index 9b21d3ba..2c6c7b30 100644 --- a/module/data/fields/action/damageField.mjs +++ b/module/data/fields/action/damageField.mjs @@ -275,10 +275,18 @@ export class DHActionDiceData extends foundry.abstract.DataModel { }; } + /** + * @returns {string} the formula associated with this damage field + */ getFormula() { - const multiplier = this.multiplier === 'flat' ? this.flatMultiplier : `@${this.multiplier}`, - bonus = this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : ''; - return this.custom.enabled ? this.custom.formula : `${multiplier ?? 1}${this.dice}${bonus}`; + if (this.custom.enabled) return this.custom.formula; + + const multiplier = this.multiplier === 'flat' ? this.flatMultiplier : `@${this.multiplier}`; + if (!multiplier) return String(this.bonus || 0); + + const dice = `${multiplier ?? 1}${this.dice}`; + const sign = this.bonus < 0 ? ' - ' : ' + '; + return this.bonus ? `${dice} ${sign} ${Math.abs(this.bonus)}` : dice; } } From effab8db0cc1815ca552a3eafb3247c785c7e037 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sun, 19 Jul 2026 03:02:57 +0200 Subject: [PATCH 5/8] [Rework] ChatMessage Damage (#2079) * Initial rework * Removed unneeded method * Removed outcommented code * Added migration * Fixed DamageActions * Semi-corrected reroll of one of multiple results on a die * Code improvement * Added aseDie.rerollResult method to specifically reroll a specific result die in a grouping * TagTeamDialog somewhat working * Added migration for TagTeamData * Fix for mean tagTeamDialog.finish * . * Improved migration * Internalised ChatDamageData.prepareRolls to its own constructor * Moved DamageTypes to roll.options * TagTeamDialog fixes to accomodate new flat damage.types structure * Changed so all default rolls become BaseRolls to access convenience functions and getters * . * Fixed so that party.tagTeam now uses a declared damageRollData(ChatDamageData) field. rollData.options.damage is retired. * Critical damage fixes * Corrected TagTeamDialog rerender logic when selecting a roll * Fixed so that DamageTypes are retained and joined together throughout TagTeamDialog * Removed some remaining types.<>.roll references * [Rework] Damage and Damage Resource split (#2094) * Changed the label of ActionSheet damage.resources to Mark Resources * Fix assigning includeBase * Localize add resource * Maybe simplify API * Fix onRollSimple * Handle erroring action update differently --------- Co-authored-by: Carlos Fernandez --- daggerheart.mjs | 5 +- lang/en.json | 5 +- module/applications/dialogs/damageDialog.mjs | 12 +- module/applications/dialogs/tagTeamDialog.mjs | 219 ++++++++-------- .../sheets-configs/action-base-config.mjs | 99 ++++--- .../sheets-configs/adversary-settings.mjs | 35 ++- module/applications/ui/chatLog.mjs | 58 ++--- module/config/generalConfig.mjs | 2 +- module/data/action/attackAction.mjs | 21 +- module/data/action/baseAction.mjs | 34 ++- module/data/action/damageAction.mjs | 7 +- module/data/actor/adversary.mjs | 12 +- module/data/actor/character.mjs | 18 +- module/data/actor/tierAdjustment.mjs | 14 +- module/data/chat-message/actorRoll.mjs | 69 +++-- module/data/chat-message/chatDamageData.mjs | 58 +++++ module/data/fields/action/damageField.mjs | 78 +++--- module/data/fields/actionField.mjs | 1 + module/data/item/weapon.mjs | 23 +- module/data/tagTeamData.mjs | 3 + module/dice/baseRoll.mjs | 4 + module/dice/damageRoll.mjs | 242 ++++++------------ module/dice/dhRoll.mjs | 9 +- module/dice/die/_module.mjs | 1 + module/dice/die/advantageDie.mjs | 4 +- module/dice/die/baseDie.mjs | 12 + module/dice/die/disadvantageDie.mjs | 4 +- module/dice/die/dualityDie.mjs | 3 +- module/dice/dualityRoll.mjs | 4 - module/documents/actor.mjs | 130 +++++----- module/helpers/handlebarsHelper.mjs | 5 +- module/helpers/utils.mjs | 13 - .../migration-handlers/2_5_2.mjs | 3 +- .../migration-handlers/2_6_0.mjs | 20 ++ .../migration-handlers/base.mjs | 44 +++- module/systemRegistration/migrations.mjs | 4 +- .../less/dialog/damage-selection/sheet.less | 7 + templates/actionTypes/cost.hbs | 2 +- templates/actionTypes/damage.hbs | 194 +++++++------- templates/actionTypes/resource.hbs | 2 +- .../dialogs/dice-roll/damageSelection.hbs | 69 +++-- .../parts/tagTeamDamageParts.hbs | 51 ++-- templates/dialogs/tagTeamDialog/result.hbs | 8 +- .../dialogs/tagTeamDialog/tagTeamMember.hbs | 16 +- .../action-settings/effect.hbs | 2 +- .../adversary-settings/attack.hbs | 2 +- templates/sheets/items/weapon/settings.hbs | 40 +-- templates/ui/chat/parts/button-part.hbs | 8 +- templates/ui/chat/parts/damage-part.hbs | 115 +++++---- templates/ui/tooltip/attack.hbs | 2 +- templates/ui/tooltip/weapon.hbs | 2 +- 51 files changed, 1007 insertions(+), 788 deletions(-) create mode 100644 module/data/chat-message/chatDamageData.mjs create mode 100644 module/dice/die/baseDie.mjs create mode 100644 module/systemRegistration/migration-handlers/2_6_0.mjs diff --git a/daggerheart.mjs b/daggerheart.mjs index f91eedbe..2c51c1f6 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -6,6 +6,7 @@ import * as documents from './module/documents/_module.mjs'; import { macros } from './module/_module.mjs'; import * as collections from './module/documents/collections/_module.mjs'; import * as dice from './module/dice/_module.mjs'; +import * as die from './module/dice/die/_module.mjs'; import * as fields from './module/data/fields/_module.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs'; @@ -23,7 +24,7 @@ import TokenManager from './module/documents/tokenManager.mjs'; CONFIG.DH = SYSTEM; CONFIG.TextEditor.enrichers.push(...enricherConfig); -CONFIG.Dice.rolls = [BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll]; +CONFIG.Dice.rolls = [Roll = BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll]; CONFIG.Dice.daggerheart = { DHRoll: DHRoll, DualityRoll: DualityRoll, @@ -38,6 +39,8 @@ CONFIG.RegionBehavior.dataModels = { }; Object.assign(CONFIG.Dice.termTypes, dice.diceTypes); +CONFIG.Dice.terms.d = die.BaseDie; +CONFIG.Dice.types = [die.BaseDie, CONFIG.Dice.terms.f]; CONFIG.Actor.documentClass = documents.DhpActor; CONFIG.Actor.dataModels = models.actors.config; diff --git a/lang/en.json b/lang/en.json index 508a771d..b65cade7 100755 --- a/lang/en.json +++ b/lang/en.json @@ -104,8 +104,10 @@ "startCountdown": "Start Countdown" }, "damage": { + "addResource": "Add Resource", "multiplier": "Multiplier", - "flatMultiplier": "Flat Multiplier" + "flatMultiplier": "Flat Multiplier", + "markResources": "Mark Resources" }, "general": { "customFormula": "Custom Formula", @@ -2483,7 +2485,6 @@ "reroll": "Reroll", "rerolled": "Rerolled", "rerollThing": "Reroll {thing}", - "resource": "Resource", "result": { "single": "Result", "plural": "Results" diff --git a/module/applications/dialogs/damageDialog.mjs b/module/applications/dialogs/damageDialog.mjs index 46d3d41f..ce613ade 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,11 @@ 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); + + if (this.config.damageFormula) + 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/applications/dialogs/tagTeamDialog.mjs b/module/applications/dialogs/tagTeamDialog.mjs index b33541a3..4c6c5339 100644 --- a/module/applications/dialogs/tagTeamDialog.mjs +++ b/module/applications/dialogs/tagTeamDialog.mjs @@ -1,4 +1,5 @@ import { ResourceUpdateMap } from '../../data/action/baseAction.mjs'; +import { ChatDamageData } from '../../data/chat-message/chatDamageData.mjs'; import { MemberData } from '../../data/tagTeamData.mjs'; import { getCritDamageBonus, shouldUseHopeFearAutomation } from '../../helpers/utils.mjs'; import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; @@ -140,7 +141,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio const hasRolled = Boolean(data.rollData); if (!hasRolled) return false; - return !data.rollData.options.hasDamage || Boolean(data.rollData.options.damage); + return !data.rollData.options.hasDamage || data.damageRollData.active; }); return context; @@ -181,7 +182,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected); const critSelected = !selectedRoll ? undefined - : (selectedRoll?.rollData?.options?.roll?.isCritical ?? false); + : (selectedRoll?.roll?.isCritical ?? false); partContext.hintText = await this.getInfoTexts(this.party.system.tagTeam.members); partContext.joinedRoll = await this.getJoinedRoll({ @@ -235,8 +236,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio } const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected); - const critSelected = !selectedRoll ? undefined : (selectedRoll?.rollData?.options?.roll?.isCritical ?? false); - const damage = data.rollData?.options?.damage; + const critSelected = !selectedRoll ? undefined : (selectedRoll?.roll?.isCritical ?? false); return { ...data, @@ -247,9 +247,9 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio hasRolled: Boolean(data.rollData), rollOptions, damageRollOptions, - damage: damage, - critDamage: await this.getCriticalDamage(damage), - useCritDamage: critSelected || (critSelected === undefined && data.rollData?.options?.roll?.isCritical) + damage: data.damageRollData, + critDamage: await this.getCriticalDamage(data.damageRollData), + useCritDamage: critSelected || (critSelected === undefined && data.roll?.isCritical) }; } @@ -379,7 +379,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio let rollIsSelected = false; for (const member of Object.values(members)) { const rollFinished = Boolean(member.rollData); - const damageFinished = member.rollData?.options?.hasDamage ? Boolean(member.rollData.options.damage) : true; + const damageFinished = member.rollData?.options?.hasDamage ? member.damageRollData.active : true; rollsAreFinished = rollsAreFinished && rollFinished && damageFinished; rollIsSelected = rollIsSelected || member.selected; @@ -540,17 +540,10 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio await action.workflow.get('damage').execute(config, null, true); if (!config.damage) return; - - const current = this.party.system.tagTeam.members[memberKey].rollData; + await this.updatePartyData( { - [`system.tagTeam.members.${memberKey}.rollData`]: { - ...current, - options: { - ...current.options, - damage: config.damage - } - } + [`system.tagTeam.members.${memberKey}.damageRollData`]: config.damage }, this.getUpdatingParts(button) ); @@ -558,15 +551,11 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio static async #removeDamageRoll(_, button) { const { memberKey } = button.dataset; - const current = this.party.system.tagTeam.members[memberKey].rollData; this.updatePartyData( { - [`system.tagTeam.members.${memberKey}.rollData`]: { - ...current, - options: { - ...current.options, - damage: null - } + [`system.tagTeam.members.${memberKey}.damageRollData`]: { + main: null, + resources: _replace({}) } }, this.getUpdatingParts(button) @@ -574,89 +563,44 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio } static async #rerollDamageDice(_, button) { - const { memberKey, damageKey, part, dice } = button.dataset; + const { isResource, memberKey, damageKey, diceIndex, resultIndex } = button.dataset; const memberData = this.party.system.tagTeam.members[memberKey]; - const partData = memberData.rollData.options.damage[damageKey].parts[part]; - const activeDiceResultKey = Object.keys(partData.dice[dice].results).find( - index => partData.dice[dice].results[index].active - ); - const { parsedRoll, rerolledDice } = await game.system.api.dice.DamageRoll.reroll( - partData, - dice, - activeDiceResultKey - ); - - const rollData = this.party.system.tagTeam.members[memberKey].rollData; - rollData.options.damage[damageKey].parts = rollData.options.damage[damageKey].parts.map((damagePart, index) => { - if (index !== Number.parseInt(part)) return damagePart; - - return { - ...damagePart, - total: parsedRoll.total, - dice: rerolledDice - }; - }); - rollData.options.damage[damageKey].total = rollData.options.damage[damageKey].parts.reduce((acc, part) => { - acc += part.total; - return acc; - }, 0); + await memberData.damageRollData.rerollDamageDie(isResource, damageKey, diceIndex, resultIndex); + const basePath = `system.tagTeam.members.${memberKey}.damageRollData`; + const updatePath = isResource ? `${basePath}.resources.${damageKey}` : `${basePath}.main`; + const updateValue = isResource ? + memberData.damageRollData.resources[damageKey] : memberData.damageRollData.main; this.updatePartyData( { - [`system.tagTeam.members.${memberKey}.rollData`]: rollData + [updatePath]: updateValue.toJSON() }, this.getUpdatingParts(button) ); } - async getCriticalDamage(damage) { - const newDamage = foundry.utils.deepClone(damage); - for (let key in newDamage) { - var damage = newDamage[key]; - damage.formula = ''; - damage.total = 0; - - for (let part of damage.parts) { - const criticalDamage = await getCritDamageBonus(part.formula); - if (criticalDamage) { - part.modifierTotal += criticalDamage; - part.total += criticalDamage; - part.formula = `${part.dice.map(x => x.formula).join(' + ')} + ${part.modifierTotal}`; - part.roll = new Roll(part.formula); - } - - damage.formula = [damage.formula, part.formula].filter(x => x).join(' + '); - damage.total += part.total; + async getCriticalDamage(origDamage) { + const newDamage = origDamage ? ChatDamageData.fromJSON(JSON.stringify(origDamage)) : null; + if (newDamage?.main) { + const criticalDamage = await getCritDamageBonus(newDamage.main.formula); + if (criticalDamage) { + const criticalTerm = new foundry.dice.terms.NumericTerm({ number: criticalDamage, evaluated: true }); + criticalTerm.evaluate(); + newDamage.main = await Roll.fromTerms([ + ...origDamage.main.terms, + new foundry.dice.terms.OperatorTerm({ operator: '+' }), + criticalTerm + ]); + newDamage.main.options = foundry.utils.deepClone(origDamage.main.options); } - } - - return newDamage; - } - - async getNonCriticalDamage(config) { - const newDamage = foundry.utils.deepClone(config.damage); - for (let key in newDamage) { - var damage = newDamage[key]; - damage.formula = ''; - damage.total = 0; - - for (let part of damage.parts) { - const critDamageBonus = await getCritDamageBonus(part.formula); - part.modifierTotal -= critDamageBonus; - part.total -= critDamageBonus; - part.formula = `${part.dice.map(x => x.formula).join(' + ')} + ${part.modifierTotal}`; - part.roll = new Roll(part.formula); - - damage.formula = [damage.formula, part.formula].filter(x => x).join(' + '); - damage.total += part.total; - } - } + } return newDamage; } static async #selectRoll(_, button) { const { memberKey } = button.dataset; + this.updatePartyData( { [`system.tagTeam.members`]: Object.entries(this.party.system.tagTeam.members).reduce( @@ -667,7 +611,12 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio {} ) }, - this.getUpdatingParts(button) + /* Selecting a roll must update all member sections hbs to display the correct damage information incase of a critical */ + [ + ...Object.keys(this.party.system.tagTeam.members), + this.constructor.PARTS.rollSelection.id, + this.constructor.PARTS.result.id + ] ); } @@ -685,29 +634,65 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio if (!baseMainRoll?.rollData || !baseSecondaryRoll) return null; const mainRoll = new MemberData(baseMainRoll.toObject()); - const secondaryRollData = new MemberData(baseSecondaryRoll.toObject()).rollData; - const systemData = mainRoll.rollData.options; - const isCritical = overrideIsCritical ?? systemData.roll.isCritical; - if (isCritical) systemData.damage = await this.getCriticalDamage(systemData.damage); + mainRoll.damageRollData = baseMainRoll.damageRollData ? + ChatDamageData.fromJSON(JSON.stringify(baseMainRoll.damageRollData)) : null; + const secondaryRoll = new MemberData(baseSecondaryRoll.toObject()); + secondaryRoll.damageRollData = baseSecondaryRoll.damageRollData ? + ChatDamageData.fromJSON(JSON.stringify(baseSecondaryRoll.damageRollData)) : null; - if (secondaryRollData?.options.hasDamage) { + const isCritical = overrideIsCritical ?? mainRoll.roll.isCritical; + if (isCritical) mainRoll.damageRollData = await this.getCriticalDamage(mainRoll.damageRollData); + + if (secondaryRoll.damageRollData) { const secondaryDamage = (displayVersion ? overrideIsCritical : isCritical) - ? await this.getCriticalDamage(secondaryRollData.options.damage) - : secondaryRollData.options.damage; - if (systemData.damage) { - for (const [key, damage] of Object.entries(secondaryDamage ?? {})) { - if (key in systemData.damage) { - systemData.damage[key].formula = [systemData.damage[key]?.formula, damage.formula] - .filter(x => x) - .join(' + '); - systemData.damage[key].total += damage.total; - systemData.damage[key].parts.push(...damage.parts); + ? await this.getCriticalDamage(secondaryRoll.damageRollData) + : secondaryRoll.damageRollData; + if (mainRoll.damageRollData) { + if (secondaryDamage.main) { + if (mainRoll.damageRollData.main) { + mainRoll.damageRollData.main = Roll.fromTerms([ + ...baseMainRoll.damageRollData.main.terms, + new foundry.dice.terms.OperatorTerm({ operator: '+' }), + ...baseSecondaryRoll.damageRollData.main.terms + ]); + + /* Joining the roll.options of both rolls */ + const joinedDamageTypes = new Set([ + ...baseMainRoll.damageRollData.main.options.damageTypes, + ...baseSecondaryRoll.damageRollData.main.options.damageTypes + ]); + mainRoll.damageRollData.main.options = { + ...baseMainRoll.damageRollData.main.options, + damageTypes: [...joinedDamageTypes] + }; } else { - systemData.damage[key] = damage; + mainRoll.damageRollData.main = secondaryDamage.main; + } + } + + for (const [key, damage] of Object.entries(secondaryDamage.resources ?? {})) { + if (key in mainRoll.damageRollData.resources) { + mainRoll.damageRollData.resources[key] = Roll.fromTerms([ + ...baseMainRoll.damageRollData.resources[key].terms, + new foundry.dice.terms.OperatorTerm({ operator: '+' }), + ...baseSecondaryRoll.damageRollData.resources[key].terms + ]); + + /* Joining the roll.options of both rolls */ + const joinedDamageTypes = new Set([ + ...baseMainRoll.damageRollData.resources[key].options.damageTypes, + ...baseSecondaryRoll.damageRollData.resources[key].options.damageTypes + ]); + mainRoll.damageRollData.resources[key].options = { + ...baseMainRoll.damageRollData.resources[key].options, + damageTypes: [...joinedDamageTypes] + }; + } else { + mainRoll.damageRollData.resources[key] = damage; } } } else { - systemData.damage = secondaryDamage; + mainRoll.damageRollData = secondaryDamage; } } @@ -762,13 +747,27 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio const mainActor = this.party.system.partyMembers.find(x => x.uuid === mainRoll.options.source.actor); mainRoll.options.title = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.chatMessageRollTitle'); + + /* This could assumably be done better. For some reason rolls don't get correctly done through rollData.toJSON */ + const systemData = { + ...mainRoll.options, + damage: joinedRoll.damageRollData?.toJSON() + }; + + if (joinedRoll.damageRollData.main) { + systemData.damage.main = joinedRoll.damageRollData.toJSON(); + } + for (const type of Object.keys(joinedRoll.damageRollData?.resources ?? {})) { + systemData.damage.resources[type] = joinedRoll.damageRollData.resources[type].toJSON(); + } + const cls = getDocumentClass('ChatMessage'), msgData = { type: 'dualityRoll', user: game.user.id, title: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.title'), speaker: cls.getSpeaker({ actor: mainActor }), - system: mainRoll.options, + system: systemData, rolls: [JSON.stringify(joinedRoll.roll)], sound: null, flags: { core: { RollTable: true } } diff --git a/module/applications/sheets-configs/action-base-config.mjs b/module/applications/sheets-configs/action-base-config.mjs index b65e1cdf..e675fb3f 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,9 @@ 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') + 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; } @@ -253,7 +255,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) const submitData = this._prepareSubmitData(event, formData); const data = foundry.utils.mergeObject(this.action.toObject(), submitData); - this.action = await this.action.update(data); + this.action = (await this.action.update(data)) ?? this.action; this.sheetUpdate?.(this.action); this.render(); @@ -299,53 +301,70 @@ 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; + /** @this DHActionBaseConfig */ + 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) }); + } + + /** @this DHActionBaseConfig */ + 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 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.Resource.single'), 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.parts.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; - - data.damage.parts[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') + title: _loc('DAGGERHEART.ACTIONS.Config.damage.addResource') }, position: { width: 300 } }); @@ -353,12 +372,12 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) typeDialog.render(true); } - static removeDamage(_event, button) { - if (!this.action.damage.parts) return; + /** @this DHActionBaseConfig */ + 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/applications/sheets-configs/adversary-settings.mjs b/module/applications/sheets-configs/adversary-settings.mjs index ff3f3039..0bf18ee6 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: this.#onAddExperience, + removeExperience: this.#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/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index 199ee87d..c457a6a6 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -110,7 +110,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo const message = game.messages.get(li.dataset.messageId); return message.system.hasRoll && (game.user.isGM || message.isAuthor); }, - callback: async li => { + onClick: async (_event, li) => { const message = game.messages.get(li.dataset.messageId); const reroll = await message.rolls[0].reroll({ liveRoll: true }); message.update({ rolls: [reroll] }); @@ -126,7 +126,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo : false; return (game.user.isGM || message.isAuthor) && hasRolledDamage; }, - callback: async li => { + onClick: async (_event, li) => { const message = game.messages.get(li.dataset.messageId); const update = await message.system.getRerolledDamage(); message.update(update); @@ -179,28 +179,15 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo } async onRollSimple(event, message) { - const buttonType = event.target.dataset.type ?? 'damage', - total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0), - damages = { - hitPoints: { - parts: [ - { - applyTo: 'hitPoints', - damageTypes: [], - total - } - ] - } - }, - targets = Array.from(game.user.targets); - + const buttonType = event.target.dataset.type ?? 'damage'; + const total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0); + const targets = Array.from(game.user.targets); if (targets.length === 0) return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected')); - - targets.forEach(target => { - if (buttonType === 'healing') target.actor.takeHealing(damages); - else target.actor.takeDamage(damages); - }); + for (const target of targets) { + if (buttonType === 'healing') target.actor.takeHealing({ hitPoints: total }); + else target.actor.takeDamage({ total }); + } } async abilityUseButton(event, message) { @@ -256,26 +243,17 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo } const message = game.messages.get(messageData._id); - const target = event.target.closest('[data-die-index]'); + const target = event.target.closest('[data-result]'); if (target.dataset.type === 'damage') { - const { damageType, part, dice, result } = target.dataset; - const damagePart = message.system.damage[damageType].parts[part]; - const { parsedRoll, rerolledDice } = await game.system.api.dice.DamageRoll.reroll(damagePart, dice, result); - const damageParts = message.system.damage[damageType].parts.map((damagePart, index) => { - if (index !== Number(part)) return damagePart; - return { - ...damagePart, - total: parsedRoll.total, - dice: rerolledDice - }; - }); - const updateMessage = game.messages.get(message._id); - await updateMessage.update({ - [`system.damage.${damageType}`]: { - total: parsedRoll.total, - parts: damageParts - } + const { isResource, damageType, dice, result } = target.dataset; + await message.system.damage.rerollDamageDie(isResource, damageType, dice, result); + + const updatePath = isResource ? `system.damage.resources.${damageType}` : 'system.damage.main'; + const updateValue = isResource ? + message.system.damage.resources[damageType] : message.system.damage.main; + await message.update({ + [updatePath]: updateValue.toJSON() }); } else { const rerollDice = message.system.roll.dice[target.dataset.dieIndex]; diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 188efafb..802f5907 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -825,7 +825,7 @@ export const refreshTypes = { export const itemAbilityCosts = { resource: { id: 'resource', - label: 'DAGGERHEART.GENERAL.resource', + label: 'DAGGERHEART.GENERAL.Resource.single', group: 'Global' }, quantity: { diff --git a/module/data/action/attackAction.mjs b/module/data/action/attackAction.mjs index 1988b1d8..dadc85f0 100644 --- a/module/data/action/attackAction.mjs +++ b/module/data/action/attackAction.mjs @@ -13,18 +13,19 @@ export default class DHAttackAction extends DHDamageAction { if (this.damage.includeBase) { const baseDamage = this.getParentHitPointDamage(); if (baseDamage) { - if (!this.damage.parts.hitPoints) { - this.damage.parts.hitPoints = baseDamage; + if (!this.damage.main) { + this.damage.main = baseDamage; } else { - for (const type of baseDamage.type) this.damage.parts.hitPoints.type.add(type); + for (const type of baseDamage.type) this.damage.main.type.add(type); - this.damage.parts.hitPoints.value.custom = { + this.damage.main.value.custom = { enabled: true, - formula: `${baseDamage.value.getFormula()} + ${this.damage.parts.hitPoints.value.getFormula()}` + formula: `${baseDamage.value.getFormula()} + ${this.damage.main.value.getFormula()}` }; } } } + if (this.roll.useDefault) { this.roll.trait = this.item.system.attack.roll.trait; this.roll.type = 'attack'; @@ -33,18 +34,18 @@ export default class DHAttackAction extends DHDamageAction { } getParentHitPointDamage() { - return this.item?.system?.attack.damage.parts.hitPoints; + return this.item?.system?.attack.damage.main; } get damageFormula() { - const hitPointsPart = this.damage.parts.hitPoints; + const hitPointsPart = this.damage.main; if (!hitPointsPart) return '0'; return hitPointsPart.value.getFormula(); } get altDamageFormula() { - const hitPointsPart = this.damage.parts.hitPoints; + const hitPointsPart = this.damage.main; if (!hitPointsPart) return '0'; return hitPointsPart.valueAlt.getFormula(); @@ -73,7 +74,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].filter(d => !!d)) { const usedValue = useAltDamage ? valueAlt : value; const damageString = Roll.replaceFormulaData(usedValue.getFormula(), this.actor?.getRollData() ?? {}); const str = damageString @@ -82,7 +83,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/action/baseAction.mjs b/module/data/action/baseAction.mjs index 5b871c9a..05890335 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -289,7 +289,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel hasEffect: this.hasEffect, hasSave: this.hasSave, onSave: this.save?.damageMod, - isDirect: !!this.damage?.direct, selectedMessageMode: game.settings.get('core', 'messageMode'), data: this.getRollData(), evaluate: this.hasRoll, @@ -307,20 +306,20 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel }; if (this.damage) { - config.isDirect = this.damage.direct; + config.isDirect = !!this.damage.main?.direct; - const groupAttackTokens = this.damage.groupAttack + const groupAttackTokens = this.damage.main?.groupAttack ? game.system.api.fields.ActionFields.DamageField.getGroupAttackTokens( this.actor.id, - this.damage.groupAttack + this.damage.main.groupAttack ) : null; config.damageOptions = { - groupAttack: this.damage.groupAttack + groupAttack: this.damage.main?.groupAttack ? { numAttackers: Math.max(groupAttackTokens.length, 1), - range: this.damage.groupAttack + range: this.damage.main.groupAttack } : null }; @@ -430,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) || !foundry.utils.isEmpty(this.damage.resources)); } get hasHealing() { - return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type === 'healing'; + return this.type === 'healing' && !foundry.utils.isEmpty(this.damage.resources); } get hasSave() { @@ -470,6 +469,25 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel return acc; }, {}); } + + 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)) { + if (partKey === 'hitPoints' && source.type !== 'healing') { + source.damage.main = { + ...part, + includeBase: source.damage.includeBase, + direct: source.damage.direct, + groupAttack: source.damage.groupAttack + }; + } else { + source.damage.resources[partKey] = part; + } + } + + delete source.damage.parts; + } } } 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/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 b39c64aa..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' } } } @@ -838,7 +836,7 @@ export default class DhCharacter extends DhCreature { isReversed: true }; - this.attack.damage.parts.hitPoints.value.custom.formula = `@prof${this.basicAttackDamageDice}${this.rules.attack.damage.bonus ? ` + ${this.rules.attack.damage.bonus}` : ''}`; + this.attack.damage.main.value.custom.formula = `@prof${this.basicAttackDamageDice}${this.rules.attack.damage.bonus ? ` + ${this.rules.attack.damage.bonus}` : ''}`; // Clamp resources (must be done last to ensure all updates occur) this.resources.clamp(); diff --git a/module/data/actor/tierAdjustment.mjs b/module/data/actor/tierAdjustment.mjs index 8b9e5bdc..4bf74a45 100644 --- a/module/data/actor/tierAdjustment.mjs +++ b/module/data/actor/tierAdjustment.mjs @@ -40,17 +40,17 @@ export function getTierAdjustedAdversary(source, tier) { // Store initial attack damage for abilities that have you deal a "standard attack" const initialAttack = { - type: source.system.attack.damage?.parts.hitPoints?.type?.toSorted(), - value: getFormula(source.system.attack.damage?.parts.hitPoints?.value) + type: source.system.attack.damage?.main?.type?.toSorted(), + value: getFormula(source.system.attack.damage?.main?.value) }; // Update damage of base attack. try { const damage = source.system.attack.damage; - if (!damage?.parts.hitPoints) throw new Error('Unexpected missing attack in adversary'); + if (!damage?.main) throw new Error('Unexpected missing damage in adversary'); for (const property of ['value', 'valueAlt']) { - const data = damage.parts.hitPoints[property]; + const data = damage.main[property]; const previousFormula = getFormula(data); const value = calculateAdjustedDamage(previousFormula, 'attack', damageMeta); applyAdjustedDamage(data, value); @@ -82,12 +82,12 @@ export function getTierAdjustedAdversary(source, tier) { // Update damage in item actions and convert all formula matches in the descriptions to the new damage for (const action of Object.values(item.system.actions)) { - if (!action.damage?.parts.hitPoints) continue; + if (!action.damage?.main) continue; try { // Apply conversions and save a record. If it matches attack damage *and* Its not in the description, use attack conversion instead const result = []; for (const property of ['value', 'valueAlt']) { - const { [property]: data, type: damageType } = action.damage.parts.hitPoints; + const { [property]: data, type: damageType } = action.damage.main; const previousFormula = getFormula(data); const isActuallyAttack = previousFormula === initialAttack.value && @@ -199,7 +199,7 @@ function calculateAdjustedDamage(formula, type, { currentDamageRange, newDamageR } /** - * Get formula from either damage parts *or* a simple formula object. + * Get formula from either damage data *or* a simple formula object. * @returns {string} the new formula data */ function getFormula(data) { diff --git a/module/data/chat-message/actorRoll.mjs b/module/data/chat-message/actorRoll.mjs index ccfe25ea..dd81952b 100644 --- a/module/data/chat-message/actorRoll.mjs +++ b/module/data/chat-message/actorRoll.mjs @@ -1,4 +1,5 @@ import { triggerChatRollFx } from '../../helpers/utils.mjs'; +import { ChatDamageData } from './chatDamageData.mjs'; const fields = foundry.data.fields; @@ -49,7 +50,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { originItem: originItemField(), action: new fields.StringField() }), - damage: new fields.ObjectField(), + damage: new fields.EmbeddedDataField(ChatDamageData), damageOptions: new fields.ObjectField(), costs: new fields.ArrayField(new fields.ObjectField()), successConsumed: new fields.BooleanField({ initial: false }) @@ -132,28 +133,21 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { }); } - /* TODO: Change how damage data is stored somehow to enable better rerolling */ async getRerolledDamage() { - if (!this.damage) return; + if (!this.damage.active) return; const rerolls = []; - const update = { system: { damage: {} } }; - for (const partKey in this.damage) { - const part = this.damage[partKey]; - const testRoll = Roll.fromData(part.parts[0].roll); - const rerolled = await testRoll.reroll(); - rerolls.push(rerolled); + const update = { system: { damage: { main: null, resources: _replace({}) } } }; + if (this.damage.main) { + const reroll = await this.damage.main.reroll(); + rerolls.push(reroll); + update.system.damage.main = reroll.toJSON(); + } - if (!update.system.damage[partKey]) update.system.damage[partKey] = { parts: [part.parts[0]] }; - const partData = update.system.damage[partKey].parts[0]; - update.system.damage[partKey].total = rerolled.total; - partData.modifierTotal = rerolled.terms.reduce((acc, x) => { - if (x.isDeterministic && !x.operator) acc += x.total; - return acc; - }, 0); - partData.dice = rerolled.dice.map(d => ({ ...d.toJSON(), dice: d.denomination })); - partData.total = rerolled.total; - partData.roll = rerolled.toJSON(); + for (const key of Object.keys(this.damage.resources)) { + const reroll = await this.damage.resources[key].reroll(); + rerolls.push(reroll); + update.system.damage.resources[key] = reroll.toJSON(); } await triggerChatRollFx(rerolls); @@ -198,6 +192,43 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { this.isGM = game.user.isGM; //temp } + static migrateData(source) { + const { main, resources, ...flatDamageKeys } = source.damage ?? {}; + if (source.damage && !main && !resources) { + source.damage.main = null; + source.damage.resources = {}; + + const getRoll = key => { + const damageData = source.damage[key]; + const oldRoll = damageData.parts[0]?.roll; + return oldRoll ? JSON.stringify({ + ...oldRoll, + class: 'BaseRoll', + options: { + ...oldRoll.options, + damageTypes: damageData.parts[0].damageTypes ?? [] + } + }) : null; + }; + + for (const key of Object.keys(flatDamageKeys)) { + if (key === 'hitPoints' && source.hasDamage && !source.hasHealing) { + source.damage.main = getRoll('hitPoints'); + } + else { + source.damage.resources[key] = getRoll(key); + } + } + } + + for (const key of Object.keys(flatDamageKeys)) { + delete source.damage[key]; + } + + return source; + } + + getTargetList() { const targets = this.targetMode && this.parent.isAuthor diff --git a/module/data/chat-message/chatDamageData.mjs b/module/data/chat-message/chatDamageData.mjs new file mode 100644 index 00000000..404cda43 --- /dev/null +++ b/module/data/chat-message/chatDamageData.mjs @@ -0,0 +1,58 @@ +import { triggerChatRollFx } from '../../helpers/utils.mjs'; + +export class ChatDamageData extends foundry.abstract.DataModel { + constructor(data = {}, options = {}) { + super(data, options); + + this._prepareRolls(); + } + + static defineSchema() { + const fields = foundry.data.fields; + + return { + main: new fields.JSONField({ nullable: true, validate: ChatDamageData.#validateRoll}), + resources: new fields.TypedObjectField(new fields.JSONField({validate: ChatDamageData.#validateRoll})) + }; + } + + get active() { + return !!this.main || Boolean(Object.keys(this.resources).length); + } + + static #validateRoll(rollJSON) { + if (rollJSON) { + const roll = JSON.parse(rollJSON); + if (!roll.evaluated) throw new Error('Roll objects added to ChatMessage documents must be evaluated'); + } + } + + _prepareRolls() { + this.main &&= Roll.fromData(this.main); + for (const key of Object.keys(this.resources)) { + this.resources[key] = Roll.fromData(this.resources[key]); + } + } + + async rerollDamageDie(isResource, damageType, dice, resultIndex) { + const reroll = isResource ? this.resources[damageType] : this.main; + const rerollDice = reroll.dice[dice]; + await rerollDice.rerollResult(resultIndex); + await reroll._evaluate(); + + const rerolledResult = rerollDice.results[rerollDice.results.length - 1]; + if (rerolledResult) { + const fakeRoll = { + _evaluated: true, + dice: [new foundry.dice.terms.Die({ + ...rerollDice, + results: [rerolledResult], + total: rerolledResult.value, + faces: rerollDice.faces + })], + options: { appearance: {} } + }; + await triggerChatRollFx([fakeRoll]); + } + } +} \ No newline at end of file diff --git a/module/data/fields/action/damageField.mjs b/module/data/fields/action/damageField.mjs index 2c6c7b30..25b3ba97 100644 --- a/module/data/fields/action/damageField.mjs +++ b/module/data/fields/action/damageField.mjs @@ -12,20 +12,10 @@ export default class DamageField extends fields.SchemaField { /** @inheritDoc */ constructor(options, context = {}) { - const damageFields = { - parts: new IterableTypedObjectField(DHDamageData), - includeBase: new fields.BooleanField({ - initial: false, - label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label' - }), - direct: new fields.BooleanField({ initial: false, label: 'DAGGERHEART.CONFIG.DamageType.direct.name' }), - groupAttack: new fields.StringField({ - choices: CONFIG.DH.GENERAL.groupAttackRange, - blank: true, - label: 'DAGGERHEART.ACTIONS.Settings.groupAttack.label' - }) - }; - super(damageFields, options, context); + super({ + main: new fields.EmbeddedDataField(DHDamageData, { nullable: true }), + resources: new IterableTypedObjectField(DHResourceData) + }, options, context); } /** @@ -41,25 +31,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) }; @@ -92,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) { @@ -114,13 +102,12 @@ export default class DamageField extends fields.SchemaField { actor.takeHealing(config.damage).then(updates => targetDamage.push({ token, updates })) ); else { - const configDamage = foundry.utils.deepClone(config.damage); - const hpDamageMultiplier = config.actionActor?.system.rules?.attack?.damage?.hpDamageMultiplier ?? 1; - const hpDamageTakenMultiplier = actor.system.rules?.attack?.damage?.hpDamageTakenMultiplier; - if (configDamage.hitPoints) { - for (const part of configDamage.hitPoints.parts) { - part.total = Math.ceil(part.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( @@ -183,13 +170,19 @@ 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.type ?? new Set(), + applyTo: x.applyTo + })); + const formattedFormulas = []; - formulas.forEach(formula => { + for (const formula of formulas) { if (isNaN(formula.formula)) formula.formula = Roll.replaceFormulaData(formula.formula, this.getRollData(data)); const same = formattedFormulas.find( @@ -197,7 +190,8 @@ export default class DamageField extends fields.SchemaField { ); if (same) same.formula += ` + ${formula.formula}`; else formattedFormulas.push(formula); - }); + } + return formattedFormulas; } @@ -294,6 +288,7 @@ export class DHResourceData extends foundry.abstract.DataModel { /** @override */ static defineSchema() { return { + base: new fields.BooleanField({ initial: false, readonly: true, label: 'Base' }), applyTo: new fields.StringField({ choices: CONFIG.DH.GENERAL.healingTypes, required: true, @@ -316,7 +311,16 @@ export class DHDamageData extends DHResourceData { static defineSchema() { return { ...super.defineSchema(), - base: new fields.BooleanField({ initial: false, readonly: true, label: 'Base' }), + includeBase: new fields.BooleanField({ + initial: false, + label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label' + }), + direct: new fields.BooleanField({ initial: false, label: 'DAGGERHEART.CONFIG.DamageType.direct.name' }), + groupAttack: new fields.StringField({ + 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, diff --git a/module/data/fields/actionField.mjs b/module/data/fields/actionField.mjs index ba2fa37e..f3bdce8a 100644 --- a/module/data/fields/actionField.mjs +++ b/module/data/fields/actionField.mjs @@ -238,6 +238,7 @@ export function ActionMixin(Base) { result = this.parent; } else { result = await this.item.update({ [path]: updates }, options); + if (!result) return result; } return this.inCollection diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 42326f93..5ff2d8d1 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,11 +228,12 @@ export default class DHWeapon extends AttachableItem { game.i18n.localize(`DAGGERHEART.CONFIG.Burden.${burden}`) ]; - for (const { value, type } of attack.damage.parts) { + if (attack.damage.main) { + const { value, type } = attack.damage.main; 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(' | '); @@ -243,7 +242,7 @@ export default class DHWeapon extends AttachableItem { tags.push(parts.join('')); } - + return tags; } @@ -258,10 +257,10 @@ 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].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/module/data/tagTeamData.mjs b/module/data/tagTeamData.mjs index 640c2f6c..ef6536f1 100644 --- a/module/data/tagTeamData.mjs +++ b/module/data/tagTeamData.mjs @@ -1,3 +1,5 @@ +import { ChatDamageData } from './chat-message/chatDamageData.mjs'; + export default class TagTeamData extends foundry.abstract.DataModel { static defineSchema() { const fields = foundry.data.fields; @@ -37,6 +39,7 @@ export class MemberData extends foundry.abstract.DataModel { }), rollChoice: new fields.StringField({ nullable: true, initial: null }), rollData: new fields.JSONField({ nullable: true, initial: null }), + damageRollData: new fields.EmbeddedDataField(ChatDamageData), selected: new fields.BooleanField({ initial: false }) }; } diff --git a/module/dice/baseRoll.mjs b/module/dice/baseRoll.mjs index 4d065fff..2c42d38a 100644 --- a/module/dice/baseRoll.mjs +++ b/module/dice/baseRoll.mjs @@ -4,4 +4,8 @@ export default class BaseRoll extends Roll { /** @inheritdoc */ static TOOLTIP_TEMPLATE = 'systems/daggerheart/templates/ui/chat/foundryRollTooltip.hbs'; + + get modifierTotal() { + return this.total - this.dice.reduce((acc, dice) => acc + dice.total, 0); + } } diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index 3f2f79e0..683d14d8 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -13,36 +13,34 @@ 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 = {}, message = {}) { - if (config.dialog.configure === false) roll.constructFormula(config); - for (const roll of config.roll) await roll.roll.evaluate(); - - roll._evaluated = true; - - const parts = []; - for (const rollData of config.roll) { - const roll = rollData.roll; - parts.push({ - ...rollData, - ...roll.options.roll, - total: roll.total, - formula: roll.formula, - dice: roll.dice.map(d => ({ - dice: d.denomination, - total: d.total, - formula: d.formula, - results: d.results - })), - damageTypes: [...(rollData.damageTypes ?? [])], - roll, - type: config.type, - modifierTotal: this.calculateTotalModifiers(roll) - }); - rollData.roll = JSON.stringify(roll.toJSON()); + static async buildEvaluate(roll, config = {}) { + if (config.dialog.configure === false) roll.constructFormulas(config); + + const evaluateRoll = async roll => { + await roll.roll.evaluate(); + roll.roll.options = { damageTypes: roll.damageTypes ? [...roll.damageTypes] : [] }; + return roll.roll; } - config.damage = this.unifyDamageRoll(parts); + if (!config.damage) config.damage = { main: null, resources: {} }; + + if (config.damageFormula) { + 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; } static async buildPost(roll, config, message) { @@ -53,9 +51,10 @@ export default class DamageRoll extends DHRoll { const diceRolls = []; if (game.modules.get('dice-so-nice')?.active) { config.mute = true; - const pool = foundry.dice.terms.PoolTerm.fromRolls( - Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll)) - ); + const pool = foundry.dice.terms.PoolTerm.fromRolls([ + ...(config.damage.main ? [config.damage.main] : []), + ...Object.values(config.damage.resources) + ]); diceRolls.push(Roll.fromTerms([pool])); } @@ -66,22 +65,14 @@ export default class DamageRoll extends DHRoll { await super.buildPost(roll, config, message); if (config.source?.message) { - chatMessage.update({ 'system.damage': config.damage }); + chatMessage.update({ 'system.damage': { + ...config.damage.toObject(), + main: config.damage.main, + resources: config.damage.resources + }}); } } - static unifyDamageRoll(rolls) { - const unified = {}; - rolls.forEach(r => { - const resource = unified[r.applyTo] ?? { formula: '', total: 0, parts: [] }; - resource.formula += `${resource.formula !== '' ? ' + ' : ''}${r.formula}`; - resource.total += r.total; - resource.parts.push(r); - unified[r.applyTo] = resource; - }); - return unified; - } - static formatGlobal(rolls) { let formula, total; const applyTo = new Set(rolls.flatMap(r => r.applyTo)); @@ -130,12 +121,10 @@ export default class DamageRoll extends DHRoll { const type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage'); const changeKeys = []; - for (const roll of this.options.roll) { - for (const damageType of roll.damageTypes?.values?.() ?? []) { - changeKeys.push(`system.bonuses.${type}.${damageType}`); - } + for (const damageType of this.options.damageFormula?.damageTypes?.values?.() ?? []) { + changeKeys.push(`system.bonuses.${type}.${damageType}`); } - + const item = this.data.parent?.items?.get(this.options.source.item); if (item) { switch (item.type) { @@ -151,62 +140,69 @@ 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) { + if (!formulaData) return null; 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)); - }); + + formulaData.roll = new Roll(Roll.replaceFormulaData(formulaData.formula, config.data)); + formulaData.roll.terms = Roll.parse(formulaData.roll.formula, config.data); + + if (formulaData.extraFormula) { + formulaData.roll.terms.push( + new foundry.dice.terms.OperatorTerm({ operator: '+' }), + ...this.constructor.parse(formulaData.extraFormula, this.options.data) + ); + } + + if (isDamage && 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)); + }); + + /* To Remove When Reaction System */ + for (const mod in config.modifiers) { + const modifier = config.modifiers[mod]; + if ( + modifier.beforeCrit === true && + (modifier.enabled || modifier.value) + ) modifier.callback(formulaData); } /* 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); - } + for (const mod in config.modifiers) { + const modifier = config.modifiers[mod]; + if (!modifier.beforeCrit && (modifier.enabled || modifier.value)) modifier.callback(formulaData); } - 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) { + if (config.damageOptions.groupAttack?.numAttackers > 1) { const damageTypes = [foundry.dice.terms.Die, foundry.dice.terms.NumericTerm]; - for (const term of part.roll.terms) { + 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 = part.roll.dice.reduce((acc, term) => acc + term._faces * term._number, 0); + if (config.isCritical) { + const total = formulaData.roll.dice.reduce((acc, term) => acc + term._faces * term._number, 0); if (total > 0) { - part.roll.terms.push(...this.formatModifier(total)); + formulaData.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); } - return this.options.roll; + + formulaData.roll._formula = this.constructor.getFormula(formulaData.roll.terms); + + return formulaData; } /* To Remove When Reaction System */ @@ -299,76 +295,4 @@ export default class DamageRoll extends DHRoll { config.modifiers = mods; return mods; } - - static async reroll(rollPart, dice, result) { - let diceIndex = 0; - let parsedRoll = game.system.api.dice.DamageRoll.fromData({ - ...rollPart.roll, - terms: rollPart.roll.terms.map(term => { - const isDie = term.class === 'Die'; - const fixedTerm = { - ...term, - ...(isDie ? { results: rollPart.dice[diceIndex].results } : {}) - }; - - if (isDie) diceIndex++; - return fixedTerm; - }), - class: 'DamageRoll', - evaluated: false - }); - - const parsedDiceTerms = Object.keys(parsedRoll.terms).reduce((acc, key) => { - const term = parsedRoll.terms[key]; - if (term instanceof CONFIG.Dice.termTypes.DiceTerm) acc[Object.keys(acc).length] = term; - return acc; - }, {}); - const term = parsedDiceTerms[dice]; - const termResult = parsedDiceTerms[dice].results[result]; - - const newIndex = parsedDiceTerms[dice].results.length; - await term.reroll(`/r1=${termResult.result}`); - - const diceRolls = []; - if (game.modules.get('dice-so-nice')?.active) { - const newResult = parsedDiceTerms[dice].results[newIndex]; - diceRolls.push({ - _evaluated: true, - dice: [ - new foundry.dice.terms.Die({ - ...term, - total: newResult.result, - faces: term._faces, - results: [newResult] - }) - ], - options: { appearance: {} } - }); - } - - await triggerChatRollFx(diceRolls); - await parsedRoll.evaluate(); - - const results = parsedRoll.dice[dice].results.map(result => ({ - ...result, - discarded: !result.active - })); - const newResult = results.splice(results.length - 1, 1); - results.splice(Number(result) + 1, 0, newResult[0]); - - const rerolledDice = parsedRoll.dice.map((x, index) => { - const isRerollDice = index === Number(dice); - if (!isRerollDice) return { ...x, dice: x.denomination }; - return { - dice: parsedRoll.dice[dice].denomination, - total: parsedRoll.dice[dice].total, - results: results.map(result => ({ - ...result, - hasRerolls: result.hasRerolls || isRerollDice - })) - }; - }); - - return { parsedRoll, rerolledDice }; - } } diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index 16472fea..c78caa4f 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -1,7 +1,8 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; import { triggerChatRollFx } from '../helpers/utils.mjs'; +import BaseRoll from './baseRoll.mjs'; -export default class DHRoll extends Roll { +export default class DHRoll extends BaseRoll { baseTerms = []; constructor(formula, data = {}, options = {}) { super(formula, data, foundry.utils.mergeObject(options, { roll: [] }, { overwrite: false })); @@ -40,6 +41,10 @@ export default class DHRoll extends Roll { return config; } + static createRollInstance(config) { + return new this(config.roll.formula, config.data, config); + } + /** * @param {Partial} config * @returns {Promise} @@ -57,7 +62,7 @@ export default class DHRoll extends Roll { 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/module/dice/die/_module.mjs b/module/dice/die/_module.mjs index 19ca951a..b84cef86 100644 --- a/module/dice/die/_module.mjs +++ b/module/dice/die/_module.mjs @@ -3,6 +3,7 @@ import HopeDie from './hopeDie.mjs'; import FearDie from './fearDie.mjs'; import AdvantageDie from './advantageDie.mjs'; import DisadvantageDie from './disadvantageDie.mjs'; +export { default as BaseDie } from './baseDie.mjs'; export const diceTypes = { DualityDie, diff --git a/module/dice/die/advantageDie.mjs b/module/dice/die/advantageDie.mjs index 9c2f0b03..66b6f124 100644 --- a/module/dice/die/advantageDie.mjs +++ b/module/dice/die/advantageDie.mjs @@ -1,4 +1,6 @@ -export default class AdvantageDie extends foundry.dice.terms.Die { +import BaseDie from './baseDie.mjs'; + +export default class AdvantageDie extends BaseDie { constructor(options) { super(options); diff --git a/module/dice/die/baseDie.mjs b/module/dice/die/baseDie.mjs new file mode 100644 index 00000000..cc88f753 --- /dev/null +++ b/module/dice/die/baseDie.mjs @@ -0,0 +1,12 @@ +export default class BaseDie extends foundry.dice.terms.Die { + async rerollResult(resultIndex) { + const result = this.results[resultIndex]; + result.rerolled = true; + result.active = false; + await this.roll({ reroll: true }); + + const rerolledResult = this.results[this.results.length - 1]; + this.results.splice(this.results.length - 1, 1); + this.results.splice(resultIndex, 0, rerolledResult); + } +} \ No newline at end of file diff --git a/module/dice/die/disadvantageDie.mjs b/module/dice/die/disadvantageDie.mjs index f56ebe96..e79845f3 100644 --- a/module/dice/die/disadvantageDie.mjs +++ b/module/dice/die/disadvantageDie.mjs @@ -1,4 +1,6 @@ -export default class DisadvantageDie extends foundry.dice.terms.Die { +import BaseDie from './baseDie.mjs'; + +export default class DisadvantageDie extends BaseDie { constructor(options) { super(options); diff --git a/module/dice/die/dualityDie.mjs b/module/dice/die/dualityDie.mjs index cc7ee75e..2f4dae77 100644 --- a/module/dice/die/dualityDie.mjs +++ b/module/dice/die/dualityDie.mjs @@ -1,6 +1,7 @@ +import BaseDie from './baseDie.mjs'; import { updateResourcesForDualityReroll } from '../helpers.mjs'; -export default class DualityDie extends foundry.dice.terms.Die { +export default class DualityDie extends BaseDie { constructor(options) { super(options); diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index 38d9315f..6503efb1 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -409,8 +409,4 @@ export default class DualityRoll extends D20Roll { return rerolled; } - - fromJSON(json) { - return super.fromJSON(json); - } } diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index f8880597..666d9ac1 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -622,6 +622,11 @@ export default class DhpActor extends Actor { return rollData; } + /** + * Checks to see if damage can be reduced in one way or another. + * @param {number} hpDamage the amount of marked hp that will be marked + * @param {Set} types a list of damage types + */ #canReduceDamage(hpDamage, types) { const { stressDamageReduction, disabledArmor, reduceSeverity, thresholdImmunities } = this.system.rules.damageReduction; @@ -648,35 +653,32 @@ export default class DhpActor extends Actor { return canUseArmor || canUseStress || hasReduceSeverity || hasThresholdImmunity; } - async takeDamage(damages, isDirect = false) { - if (Hooks.call(`${CONFIG.DH.id}.preTakeDamage`, this, damages) === false) return null; + async takeDamage(args, isDirect = false) { + args = this.#parseDamageArgs(args); + if (Hooks.call(`${CONFIG.DH.id}.preTakeDamage`, this, args) === false) return null; if (this.type === 'companion') { await this.modifyResource([{ value: 1, key: 'stress' }]); return; } - const updates = []; + const updates = args.resourceUpdates; + if (args.main) { + // todo: avoid side effects, but hook currently requires it + args.main.value = this.calculateDamage(args.main.value, args.main.damageTypes); + } - Object.entries(damages).forEach(([key, damage]) => { - damage.parts.forEach(part => { - if (part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) - part.total = this.calculateDamage(part.total, part.damageTypes); - const update = updates.find(u => u.key === key); - if (update) { - update.value += part.total; - update.damageTypes.add(...new Set(part.damageTypes)); - } else updates.push({ value: part.total, key, damageTypes: new Set(part.damageTypes) }); - }); - }); + if (Hooks.call(`${CONFIG.DH.id}.postCalculateDamage`, this, args) === false) return null; - if (Hooks.call(`${CONFIG.DH.id}.postCalculateDamage`, this, damages) === false) return null; + // Convert deducted resources to a record of updates. Return if nothing to do. + if (!updates.some(u => u.value) && !args.main) return; - if (!updates.length) return; - - const hpDamage = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id); - if (hpDamage?.value) { - hpDamage.value = this.convertDamageToThreshold(hpDamage.value); + if (args.main) { + const hpDamage = { + value: this.convertDamageToThreshold(args.main.value), + damageTypes: new Set(args.main.damageTypes), + key: CONFIG.DH.GENERAL.healingTypes.hitPoints.id + }; if (this.type === 'character' && !isDirect && this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes)) { const armorSlotResult = await this.owner.query( 'armorSlot', @@ -691,7 +693,7 @@ export default class DhpActor extends Actor { ); if (armorSlotResult) { const { modifiedDamage, armorChanges, stressSpent } = armorSlotResult; - updates.find(u => u.key === 'hitPoints').value = modifiedDamage; + hpDamage.value = modifiedDamage; for (const armorChange of armorChanges) { updates.push({ value: armorChange.amount, key: 'armor', uuid: armorChange.uuid }); } @@ -701,20 +703,24 @@ export default class DhpActor extends Actor { else updates.push({ value: stressSpent, key: 'stress' }); } } - } - if (this.type === 'adversary') { + } else 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); - - if ( - hpDamage.value && - this.system.rules.damageReduction.thresholdImmunities[getDamageKey(hpDamage.value)] - ) { - hpDamage.value -= 1; + if (this.system.rules.damageReduction.thresholdImmunities[getDamageKey(hpDamage.value)]) { + hpDamage.value = Math.max(0, hpDamage.value - 1); } } + + // Merge existing hitPoint deduction with finalised damage deduction + const existing = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id); + if (existing) { + existing.value += hpDamage.value; + existing.damageTypes = hpDamage.damageTypes; + } else { + updates.push(hpDamage); + } } const results = await game.system.registeredTriggers.runTrigger( @@ -729,12 +735,10 @@ export default class DhpActor extends Actor { for (var result of results) resourceMap.addResources(result.updates); resourceMap.updateResources(); } - - updates.forEach( - u => - (u.value = - u.key === 'fear' || this.system?.resources?.[u.key]?.isReversed === false ? u.value * -1 : u.value) - ); + + for (const u of updates) { + u.value = u.key === 'fear' || this.system?.resources?.[u.key]?.isReversed === false ? u.value * -1 : u.value; + } await this.modifyResource(updates); @@ -743,6 +747,36 @@ export default class DhpActor extends Actor { return updates; } + async takeHealing(args) { + args = this.#parseDamageArgs({ resources: 'resources' in args ? args.resources : args }); + if (Hooks.call(`${CONFIG.DH.id}.preTakeHealing`, this, args) === false) return null; + + const updates = args.resourceUpdates; + for (const u of updates) { + const shouldFlip = !(u.key === 'fear' || this.system?.resources?.[u.key]?.isReversed === false); + u.value = shouldFlip ? u.value * -1 : u.value; + } + await this.modifyResource(updates); + + if (Hooks.call(`${CONFIG.DH.id}.postTakeHealing`, this, updates) === false) return null; + + return updates; + } + + /** Parse damage args that may be coming from takeHealing or takeDamage. Used to simplify macro usage and roll vs non-roll usage */ + #parseDamageArgs(args = {}) { + const damageRoll = 'total' in args ? args : (args.main ?? args.damage); + const damageValue = typeof damageRoll === 'number' ? damageRoll : damageRoll?.total; + const damageTypes = Array.from(damageRoll?.options?.damageTypes ?? damageRoll?.damageTypes ?? []); + const main = typeof damageValue === 'number' ? { key: 'damage', value: damageValue, damageTypes } : null; + const resourceUpdates = Object.entries(args.resources ?? {}).map(([key, damage]) => ({ + key, + value: typeof damage === 'number' ? damage : damage?.total ?? 0 + })); + + return { main, resourceUpdates }; + } + calculateDamage(baseDamage, type) { if (this.canResist(type, 'immunity')) return 0; if (this.canResist(type, 'resistance')) baseDamage = Math.ceil(baseDamage / 2); @@ -767,32 +801,6 @@ export default class DhpActor extends Actor { return reduction === Infinity ? 0 : reduction; } - 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; - - return updates; - } - /** * Resources are modified asynchronously, so be careful not to update the same resource in * quick succession. diff --git a/module/helpers/handlebarsHelper.mjs b/module/helpers/handlebarsHelper.mjs index 7f30d970..dbcc50dc 100644 --- a/module/helpers/handlebarsHelper.mjs +++ b/module/helpers/handlebarsHelper.mjs @@ -48,9 +48,8 @@ export default class RegisterHandlebarsHelpers { return formula; } - static damageSymbols(damageParts) { - const allTypes = [...new Set([...damageParts].flatMap(x => Array.from(x.type)))]; - const symbols = allTypes.map(p => CONFIG.DH.GENERAL.damageTypes[p].icon); + static damageSymbols(damageData) { + const symbols = damageData.type.map(p => CONFIG.DH.GENERAL.damageTypes[p].icon); return new Handlebars.SafeString(Array.from(symbols).map(symbol => ``)); } 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/module/systemRegistration/migration-handlers/2_5_2.mjs b/module/systemRegistration/migration-handlers/2_5_2.mjs index 944f0eec..096f4bb7 100644 --- a/module/systemRegistration/migration-handlers/2_5_2.mjs +++ b/module/systemRegistration/migration-handlers/2_5_2.mjs @@ -7,7 +7,8 @@ export class Migration_2_5_2 extends MigrationHandlerBase { async updateActiveEffectSource(effectSource, item) { let shouldUpdate = false; const newChanges = []; - const srdItem = item?._stats.compendiumSource ? + + const srdItem = item?._stats?.compendiumSource ? await foundry.utils.fromUuid(item?._stats.compendiumSource) : null; for (let i = 0; i < effectSource.system.changes.length; i++) { diff --git a/module/systemRegistration/migration-handlers/2_6_0.mjs b/module/systemRegistration/migration-handlers/2_6_0.mjs new file mode 100644 index 00000000..9edda9a2 --- /dev/null +++ b/module/systemRegistration/migration-handlers/2_6_0.mjs @@ -0,0 +1,20 @@ +import { MigrationHandlerBase } from './base.mjs'; + +export class Migration_2_6_0 extends MigrationHandlerBase { + version = '2.6.0'; + + /** @inheritdoc */ + async updateActorSource(actor) { + if (actor.type === 'party' && Object.keys(actor.system.tagTeam.members).length) { + return { + _id: actor._id, + system: { + tagTeam: { + initiator: null, + members: _replace({}) + } + } + }; + } + } +} \ No newline at end of file diff --git a/module/systemRegistration/migration-handlers/base.mjs b/module/systemRegistration/migration-handlers/base.mjs index 7426570d..29c181ce 100644 --- a/module/systemRegistration/migration-handlers/base.mjs +++ b/module/systemRegistration/migration-handlers/base.mjs @@ -1,8 +1,9 @@ /** * @import DHItem from "../../documents/item.mjs"; +* @import DhActor from "../../documents/actor.mjs"; */ - /** + * The base class of an async migration. * These are generally run between versions for things that require compendiums or must be done in post. * The migrate() functions calls the various updateXSource() functions. @@ -22,6 +23,16 @@ export class MigrationHandlerBase { return null; } + /** + * Update a world actor + * @param {DhActor} actor + * @returns {Promise} + * @protected + */ + async updateActorSource(actor) { + return null; + } + async migrate() { // todo: handle more than just migrating effects. Right now this can only migrate effects // NOTE: the preload is hardcoded, we should not hardcode it @@ -60,10 +71,39 @@ export class MigrationHandlerBase { } }; - for (const actor of game.actors) { + const updateActor = async actor => { + const actorUpdate = await this.updateActorSource(actor); + if (actorUpdate) { + batch.push({ + action: 'update', + documentName: 'Actor', + updates: [actorUpdate] + }); + } + + const aeUpdates = []; for (const item of actor.items) { await updateItem(item); } + + for (const effect of actor.effects) { + const changes = await this.updateActiveEffectSource(effect.toObject(), { parent: actor }); + if (changes) aeUpdates.push(changes); + } + if (aeUpdates.length) { + batch.push({ + action: 'update', + documentName: 'ActiveEffect', + updates: aeUpdates, + parent: actor + }); + } + } + + + for (const actor of game.actors) { + await updateActor(actor); + progress.advance(); } for (const item of game.items) { diff --git a/module/systemRegistration/migrations.mjs b/module/systemRegistration/migrations.mjs index fef97b8f..af11cd2c 100644 --- a/module/systemRegistration/migrations.mjs +++ b/module/systemRegistration/migrations.mjs @@ -1,5 +1,6 @@ import { defaultRestOptions } from '../config/generalConfig.mjs'; import { Migration_2_5_2 } from './migration-handlers/2_5_2.mjs'; +import { Migration_2_6_0 } from './migration-handlers/2_6_0.mjs'; export async function runMigrations() { let lastMigrationVersion = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion); @@ -329,7 +330,8 @@ export async function runMigrations() { /* -------------------------------------------- */ const migrations = [ - new Migration_2_5_2() + new Migration_2_5_2(), + new Migration_2_6_0() ].filter(m => m.version && foundry.utils.isNewerVersion(m.version, lastMigrationVersion)); for (const handler of migrations) { diff --git a/styles/less/dialog/damage-selection/sheet.less b/styles/less/dialog/damage-selection/sheet.less index 9f8cfc8a..0bb0f2be 100644 --- a/styles/less/dialog/damage-selection/sheet.less +++ b/styles/less/dialog/damage-selection/sheet.less @@ -17,6 +17,13 @@ } } + .section-header { + font-size: var(--font-size-20); + color: light-dark(@dark, @beige); + text-align: center; + margin-bottom: -12px; + } + .bonuses { gap: 4px; .critical-chip { diff --git a/templates/actionTypes/cost.hbs b/templates/actionTypes/cost.hbs index 7a9f33d9..b8661b5b 100644 --- a/templates/actionTypes/cost.hbs +++ b/templates/actionTypes/cost.hbs @@ -9,7 +9,7 @@ {{/if}}
    {{formField ../fields.scalable label="DAGGERHEART.GENERAL.scalable" value=cost.scalable name=(concat "cost." index ".scalable") classes="checkbox" localize=true}} - {{formField ../fields.key choices=(@root.disableOption index @root.costOptions ../source) label="DAGGERHEART.GENERAL.resource" value=cost.key name=(concat "cost." index ".key") localize=true blank=false}} + {{formField ../fields.key choices=(@root.disableOption index @root.costOptions ../source) label="DAGGERHEART.GENERAL.Resource.single" value=cost.key name=(concat "cost." index ".key") localize=true blank=false}} {{formField ../fields.value label="DAGGERHEART.GENERAL.amount" value=cost.value name=(concat "cost." index ".value") localize=true}} {{formField ../fields.step label="DAGGERHEART.GENERAL.step" value=cost.step name=(concat "cost." index ".step") disabled=(not cost.scalable) localize=true}} diff --git a/templates/actionTypes/damage.hbs b/templates/actionTypes/damage.hbs index 03300840..d430b51f 100644 --- a/templates/actionTypes/damage.hbs +++ b/templates/actionTypes/damage.hbs @@ -1,92 +1,114 @@ - -
    - - {{#if (eq @root.source.type 'healing')}} - {{localize "DAGGERHEART.GENERAL.healing"}} - {{else}} +{{#unless (eq @root.source.type 'healing')}} +
    + {{localize "DAGGERHEART.GENERAL.damage"}} + {{#if source.main}} + + {{else}} + + {{/if}} + + + {{#if source.main}} +
    + {{#if @root.hasBaseDamage}} + {{formField @root.fields.damage.fields.main.fields.includeBase value=source.main.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}} + {{#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 (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}} -
    +
    +{{/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.parts 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}} - -
    -
    - {{/each}} - +{{#unless (eq path 'system.attack.')}} + {{! In the future, consider allowing this even on NPCs}} +
    + + {{#if (eq @root.source.type 'healing')}} + {{localize "DAGGERHEART.GENERAL.healing"}} + {{else}} + {{localize "DAGGERHEART.ACTIONS.Config.damage.markResources"}} + {{/if}} + {{#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 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 @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}} -
    - {{/if}} - {{#if @root.isNPC}} - - {{/if}} + {{#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 ".custom.formula") localize=true}} + {{else}} +
    + {{#unless @root.isNPC}} + {{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 ".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}} + + {{/if}} +{{/inline}} + +{{#*inline "damageData"}} + {{#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 damage.base) damage.resultBased)}} +
    +
    + {{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.hope")}} + {{> 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 key=damage.applyTo fields=fields.valueAlt.fields type=fields.type isBase=damage.base source=damage.valueAlt basePath=(concat basePath ".valueAlt")}} +
    +
    + {{else}} + {{> formula key=damage.applyTo fields=fields.value.fields type=fields.type isBase=damage.base source=damage.value basePath=(concat basePath ".value")}} + {{/if}} + +{{/inline}} + +{{#*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/actionTypes/resource.hbs b/templates/actionTypes/resource.hbs index 9c8fc965..97304b9a 100644 --- a/templates/actionTypes/resource.hbs +++ b/templates/actionTypes/resource.hbs @@ -1,7 +1,7 @@
    -
    {{localize "DAGGERHEART.GENERAL.resource"}}
    +
    {{localize "DAGGERHEART.GENERAL.Resource.single"}}
    diff --git a/templates/dialogs/dice-roll/damageSelection.hbs b/templates/dialogs/dice-roll/damageSelection.hbs index 7bcd7063..e865ebe6 100644 --- a/templates/dialogs/dice-roll/damageSelection.hbs +++ b/templates/dialogs/dice-roll/damageSelection.hbs @@ -15,34 +15,7 @@ {{/each}}
    {{/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}} - -
    -
    - - -
    - {{/each}} - + {{#if damageOptions.groupAttack}}
    {{localize "DAGGERHEART.ACTIONS.Settings.groupAttack.label"}} @@ -59,6 +32,45 @@
    {{/if}} + + {{#if @root.damageFormula}} + {{#with @root.damageFormula}} +
    + {{localize "DAGGERHEART.GENERAL.formula"}}: {{roll.formula}} + + {{localize "DAGGERHEART.GENERAL.damage"}} + {{#if damageTypes}} + {{#each damageTypes as | type | }} + {{#with (lookup @root.config.GENERAL.damageTypes type)}} + + {{/with}} + {{/each}} + {{/if}} + +
    +
    + + +
    + {{/with}} + {{/if}} + + {{#each @root.resourceFormulas}} +
    + {{localize "DAGGERHEART.GENERAL.formula"}}: {{roll.formula}} + + {{#with (lookup @root.config.GENERAL.healingTypes applyTo)}} + {{localize label}} + {{/with}} + +
    +
    + +
    + {{/each}} {{#unless (empty @root.modifiers)}}
    @@ -76,6 +88,7 @@ {{/each}}
    {{/unless}} +
    {{#if directDamage}} - {{/with}} + {{#with systemFields.attack.fields.damage.fields.main.fields as | fields | }} + {{#with ../document.system.attack.damage.main as | source | }} + {{localize "DAGGERHEART.GENERAL.damage"}} + {{localize "DAGGERHEART.ACTIONS.Config.general.customFormula"}} + {{formInput fields.value.fields.custom.fields.enabled value=source.value.custom.enabled name="system.attack.damage.main.value.custom.enabled"}} + {{#if source.value.custom.enabled}} + {{localize "DAGGERHEART.ACTIONS.Config.general.formula"}} + {{formInput fields.value.fields.custom.fields.formula value=source.value.custom.formula name="system.attack.damage.main.value.custom.formula"}} + {{else}} + {{localize "DAGGERHEART.GENERAL.Dice.single"}} + {{formInput fields.value.fields.dice value=source.value.dice name="system.attack.damage.main.value.dice"}} + {{localize "DAGGERHEART.GENERAL.bonus"}} + {{formInput fields.value.fields.bonus value=source.value.bonus name="system.attack.damage.main.value.bonus" localize=true}} + {{/if}} + {{localize "DAGGERHEART.GENERAL.type"}} + {{formInput fields.type value=source.type name="system.attack.damage.main.type" localize=true}} + {{localize "DAGGERHEART.CONFIG.DamageType.direct.name"}} + {{formInput @root.systemFields.attack.fields.damage.fields.main.fields.direct value=@root.document.system.attack.damage.main.direct name="system.attack.damage.main.direct" localize=true}} + + {{/with}} {{/with}}
    diff --git a/templates/ui/chat/parts/button-part.hbs b/templates/ui/chat/parts/button-part.hbs index 6bc5f372..a0075b5f 100644 --- a/templates/ui/chat/parts/button-part.hbs +++ b/templates/ui/chat/parts/button-part.hbs @@ -1,18 +1,18 @@
    {{#if areas.length}}{{/if}} {{#if hasDamage}} - {{#unless (empty damage)}} + {{#if damage.active}} {{else}} - {{/unless}} + {{/if}} {{/if}} {{#if hasHealing}} - {{#unless (empty damage)}} + {{#if damage.active}} {{else}} - {{/unless}} + {{/if}} {{/if}} {{#if (and hasEffect)}}{{/if}}
    \ No newline at end of file diff --git a/templates/ui/chat/parts/damage-part.hbs b/templates/ui/chat/parts/damage-part.hbs index 45b09b72..7d6d983d 100644 --- a/templates/ui/chat/parts/damage-part.hbs +++ b/templates/ui/chat/parts/damage-part.hbs @@ -1,4 +1,4 @@ -
    +
    {{#if hasHealing}} @@ -10,60 +10,79 @@
    - {{#each damage as | roll index | }} -
    {{localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.inChatRoll')}}: {{total}}
    + {{#if damage.main}} + {{> formula roll=damage.main label=(localize "DAGGERHEART.GENERAL.damage") }} + {{/if}} + {{#each damage.resources as | roll index | }} + {{> formula roll=roll label=(ifThen ../hasHealing (localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.name')) (localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.inChatRoll'))) }} {{/each}}
    - {{#each damage as | roll index | }} -
    - - {{#if ../hasHealing}}{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.name')}}{{else}}{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.inChatRoll')}}{{/if}}
    {{localize "DAGGERHEART.GENERAL.total"}}: {{roll.total}}
    {{#if (and (eq index "hitPoints") ../isDirect)}}
    {{localize "DAGGERHEART.CONFIG.DamageType.direct.short"}}
    {{/if}} -
    - {{#each roll.parts}} - {{#if (and (not @root.hasHealing) damageTypes.length)}} - - {{/if}} -
    - {{#if dice.length}} - {{#each dice}} - {{#each results}} - {{#unless discarded}} -
    -
    - {{#if hasRerolls}}{{/if}} - {{result}} -
    -
    - {{/unless}} - {{/each}} - {{/each}} - {{#if modifierTotal}} -
    -
    {{modifierTotal}}
    -
    - {{/if}} - {{else}} -
    -
    {{total}}
    -
    - {{/if}} -
    - {{/each}} -
    + {{#if damage.main}} + {{> damage label=(localize "DAGGERHEART.GENERAL.damage") roll=damage.main isDirect=isDirect }} + {{/if}} + + {{#each damage.resources as | roll index | }} + {{> damage + label=(ifThen ../hasHealing (localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.name')) (localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.inChatRoll'))) + roll=roll + isResource=true + }} {{/each}}
    -
    \ No newline at end of file +
    + +{{#*inline "formula"}} +
    {{label}}: {{roll.total}}
    +{{/inline}} + +{{#*inline "damage"}} +
    + + {{label}} +
    {{localize "DAGGERHEART.GENERAL.total"}}: {{roll.total}}
    {{#if isDirect}}
    {{localize "DAGGERHEART.CONFIG.DamageType.direct.short"}}
    {{/if}} +
    + {{#if (and (not @root.hasHealing) roll.options.damageTypes.length)}} + + {{/if}} +
    + {{#if roll.dice.length}} + {{#each roll.dice}} + {{#each results}} + {{#if active}} +
    +
    + {{#if hasRerolls}}{{/if}} + {{result}} +
    +
    + {{/if}} + {{/each}} + {{/each}} + + {{#if roll.modifierTotal}} +
    +
    {{roll.modifierTotal}}
    +
    + {{/if}} + {{else}} +
    +
    {{roll.total}}
    +
    + {{/if}} +
    +
    +{{/inline}} \ No newline at end of file diff --git a/templates/ui/tooltip/attack.hbs b/templates/ui/tooltip/attack.hbs index 8e4a1bb0..903c13a5 100644 --- a/templates/ui/tooltip/attack.hbs +++ b/templates/ui/tooltip/attack.hbs @@ -23,7 +23,7 @@
    {{/if}}
    - {{{damageFormula attack}}} {{{damageSymbols attack.damage.parts}}} + {{{damageFormula attack}}} {{{damageSymbols attack.damage.main}}}
    {{#if description}} diff --git a/templates/ui/tooltip/weapon.hbs b/templates/ui/tooltip/weapon.hbs index 4adb9c46..10889015 100644 --- a/templates/ui/tooltip/weapon.hbs +++ b/templates/ui/tooltip/weapon.hbs @@ -23,7 +23,7 @@ {{/with}}
    - {{{damageFormula item.system.attack}}} {{{damageSymbols item.system.attack.damage.parts}}} + {{{damageFormula item.system.attack}}} {{{damageSymbols item.system.attack.damage.main}}}
    {{#if description}} From 3efdcb6c9a92b8f5f861be4dc81f7405abc68e2e Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Sun, 19 Jul 2026 05:35:55 -0400 Subject: [PATCH 6/8] Allow deleting countdowns via right click context menu on the main panel (#2090) --- daggerheart.d.ts | 13 +++++ module/applications/ui/countdownEdit.mjs | 1 - module/applications/ui/countdowns.mjs | 66 +++++++++++++----------- module/config/settingsConfig.mjs | 3 ++ module/config/system.mjs | 1 + module/data/_types.d.ts | 7 +++ module/data/countdowns.mjs | 63 +++++++++++++++++++--- 7 files changed, 118 insertions(+), 36 deletions(-) create mode 100644 module/data/_types.d.ts diff --git a/daggerheart.d.ts b/daggerheart.d.ts index 891a3a2a..02abf063 100644 --- a/daggerheart.d.ts +++ b/daggerheart.d.ts @@ -11,6 +11,9 @@ import * as documents from './module/documents/_module.mjs'; import { macros } from './module/_module.mjs'; import * as dice from './module/dice/_module.mjs'; import * as fields from './module/data/fields/_module.mjs'; +import { gameSettings } from './module/config/settingsConfig.mjs'; +import DhCountdowns from './module/data/countdowns.mjs'; +import DhAutomation from './module/data/settings/Automation.mjs'; // Foundry's use of `Object.assign(globalThis) means many globally available objects are not read as such @@ -103,3 +106,13 @@ declare module '@client/packages/system.mjs' { }; } } + +declare module '@client/helpers/client-settings.mjs' { + // Add explicit typed overrides for auto complete. These require /** @type {"string"} on the vars themselves to work */ + export default interface ClientSettings { + get(namespace: 'daggerheart', key: typeof gameSettings.Automation): DhAutomation; + get(namespace: 'daggerheart', key: typeof gameSettings.Homebrew): DhHomebrew; + get(namespace: 'daggerheart', key: typeof gameSettings.Countdowns): DhCountdowns; + get(namespace: 'daggerheart', key: string): unknown; + } +} \ No newline at end of file diff --git a/module/applications/ui/countdownEdit.mjs b/module/applications/ui/countdownEdit.mjs index 1dbc56be..af1af46b 100644 --- a/module/applications/ui/countdownEdit.mjs +++ b/module/applications/ui/countdownEdit.mjs @@ -153,7 +153,6 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio action: socketEvent.Refresh, data: { refreshType: RefreshType.Countdown } }); - Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown }); } static #addCountdown() { diff --git a/module/applications/ui/countdowns.mjs b/module/applications/ui/countdowns.mjs index 5cf79100..996a46ea 100644 --- a/module/applications/ui/countdowns.mjs +++ b/module/applications/ui/countdowns.mjs @@ -12,14 +12,11 @@ const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; export default class DhCountdowns extends HandlebarsApplicationMixin(ApplicationV2) { previousCountdownData = null; - changedCountdownsForAnimation = new Set(); constructor(options = {}) { super(options); - this.previousCountdownData = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns).countdowns; - this.setupHooks(); } /** @inheritDoc */ @@ -80,7 +77,15 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application return frame; } + /** @inheritdoc */ + async _onFirstRender(context, options) { + await super._onFirstRender(context, options); + this._createContextMenu(this._getCountdownContextOptions, '.countdown-container[data-countdown]', { + parentClassHooks: false, fixed: true + }); + } + /** @inheritdoc */ async _onRender(context, options) { await super._onRender(context, options); @@ -95,7 +100,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application /* Handle animations to draw attention to countdown values changing */ const typesToAnimate = new Set(); - for (const countdownKey of this.changedCountdownsForAnimation) { + for (const countdownKey of options.animate ?? []) { const shimmerAnimation = [ { backgroundPositionX: '98%' }, { backgroundPositionX: '0%' } @@ -127,8 +132,6 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application const element = this.element.querySelector(`.header-type-toggles .header-type[data-type="${type}"]`); element?.animate(pulseAnimation, pulseTiming); } - - this.changedCountdownsForAnimation.clear(); } /** Returns countdown data filtered by ownership */ @@ -137,17 +140,15 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application const values = Object.entries(setting.countdowns).map(([key, countdown]) => ({ key, countdown, - ownership: DhCountdowns.#getPlayerOwnership(game.user, setting, countdown) + ownership: countdown.getUserLevel(game.user) })); return values.filter(v => v.ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE); } _getCountdownData() { - const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); - return this.#getCountdowns().reduce((acc, { key, countdown, ownership }) => { const playersWithAccess = game.users.reduce((acc, user) => { - const ownership = DhCountdowns.#getPlayerOwnership(user, setting, countdown); + const ownership = countdown.getUserLevel(user); if (!user.isGM && ownership && ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) { acc.push(user); } @@ -213,19 +214,6 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application return context; } - static #getPlayerOwnership(user, setting, countdown) { - if (user.isGM) return CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER; - - const playerOwnership = countdown.ownership[user.id]; - return playerOwnership === undefined || playerOwnership === CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT - ? setting.defaultOwnership - : playerOwnership; - } - - cooldownRefresh = ({ refreshType }) => { - if (refreshType === RefreshType.Countdown) this.render(); - }; - static canPerformEdit() { if (game.user.isGM) return true; @@ -323,18 +311,12 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application action: socketEvent.Refresh, data: { refreshType: RefreshType.Countdown } }); - Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown }); - } - - setupHooks() { - Hooks.on(socketEvent.Refresh, this.cooldownRefresh.bind()); } async close(options) { /* Opt out of Foundry's standard behavior of closing all application windows marked as UI when Escape is pressed */ if (options.closeKey) return; - Hooks.off(socketEvent.Refresh, this.cooldownRefresh); return super.close(options); } @@ -377,4 +359,30 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application refreshType: RefreshType.Countdown }); } + + /** + * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} + */ + _getCountdownContextOptions() { + /** @param {HTMLElement} element */ + const getCountdownFromElement = element => { + const id = element.closest('[data-countdown]').dataset.countdown; + if (!id) return null; + const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); + return setting.countdowns[id ?? '']; + } + + return [ + { + label: 'CONTROLS.CommonDelete', + icon: 'fa-solid fa-trash', + visible: element => { + return getCountdownFromElement(element)?.isOwner; + }, + onClick: (_, target) => { + getCountdownFromElement(target)?.delete(); + } + } + ]; + } } diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index 50841084..7aa1b0f7 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -27,8 +27,10 @@ export const menu = { }; export const gameSettings = { + /** @type {'Automation'} */ Automation: 'Automation', Metagaming: 'Metagaming', + /** @type {'Homebrew'} */ Homebrew: 'Homebrew', appearance: 'Appearance', GlobalOverrides: 'GlobalOverrides', @@ -37,6 +39,7 @@ export const gameSettings = { Fear: 'ResourcesFear' }, LevelTiers: 'LevelTiers', + /** @type {'Countdowns'} */ Countdowns: 'Countdowns', LastMigrationVersion: 'LastMigrationVersion', SpotlightRequestQueue: 'SpotlightRequestQueue', diff --git a/module/config/system.mjs b/module/config/system.mjs index 31dba518..de6630ec 100644 --- a/module/config/system.mjs +++ b/module/config/system.mjs @@ -12,6 +12,7 @@ import * as HOOKS from './hooksConfig.mjs'; import * as TRIGGER from './triggerConfig.mjs'; import * as ITEMBROWSER from './itemBrowserConfig.mjs'; +/** @type {"daggerheart"} */ export const SYSTEM_ID = 'daggerheart'; export const SYSTEM = { diff --git a/module/data/_types.d.ts b/module/data/_types.d.ts new file mode 100644 index 00000000..f0e2ade5 --- /dev/null +++ b/module/data/_types.d.ts @@ -0,0 +1,7 @@ +import { DhCountdown } from './countdowns.mjs' + +declare module './countdowns.mjs' { + export default interface DhCountdowns { + countdowns: Record; + } +} \ No newline at end of file diff --git a/module/data/countdowns.mjs b/module/data/countdowns.mjs index ffe4d26b..5fe01e20 100644 --- a/module/data/countdowns.mjs +++ b/module/data/countdowns.mjs @@ -1,3 +1,4 @@ +import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs'; import FormulaField from './fields/formulaField.mjs'; export default class DhCountdowns extends foundry.abstract.DataModel { @@ -14,19 +15,36 @@ export default class DhCountdowns extends foundry.abstract.DataModel { }; } - handleChange() { + /** @inheritdoc */ + _initialize(options) { + super._initialize(options); + for (const [id, countdown] of Object.entries(this.countdowns)) { + countdown.id = id; + } + } + + async handleChange() { const previousCountdowns = foundry.ui.countdowns.previousCountdownData; const changedCountdowns = Object.entries(this.countdowns).reduce((acc, [key, countdown]) => { - const previousCountdown = previousCountdowns[key]; - if (!previousCountdown || (previousCountdown.progress.current !== countdown.progress.current)) { + const previous = previousCountdowns[key]; + const currentChanged = !previous || (previous.progress.current !== countdown.progress.current); + if (currentChanged && previous?.progress.start === countdown.progress.start) { acc.push(key); } - return acc; }, []); - for (const countdownKey of changedCountdowns) - foundry.ui.countdowns.changedCountdownsForAnimation.add(countdownKey); + // Re-render countdowns applications. When the change is due to an actual update, resync the editor + if (!foundry.utils.equals(previousCountdowns, this.countdowns)) { + await foundry.ui.countdowns.render({ animate: changedCountdowns }); + for (const instance of game.system.api.applications.ui.CountdownEdit.instances()) { + instance.data = this; + await instance.render(); + } + } + + // Inform modules of updates + Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown }); } static migrateData(source) { @@ -160,6 +178,14 @@ export class DhCountdown extends foundry.abstract.DataModel { }, {}); } + /** + * A boolean indicator for whether the current game User has ownership rights for this countdown + * @returns {boolean} + */ + get isOwner() { + return this.getUserLevel(game.user) === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER; + } + /** @inheritDoc */ static migrateData(source) { if (source.progress.max) { @@ -170,4 +196,29 @@ export class DhCountdown extends foundry.abstract.DataModel { return super.migrateData(source); } + + /** + * Get the explicit permission level that a User has over this Document, a value in CONST.DOCUMENT_OWNERSHIP_LEVELS. + * Compendium content ignores the ownership field in favor of User role-based ownership. Otherwise, Documents use + * granular per-User ownership definitions and Embedded Documents defer to their parent ownership. + * + * @param {BaseUser} [user=game.user] The User being tested + * @returns {DocumentOwnershipNumber} A numeric permission level from {@link CONST.DOCUMENT_OWNERSHIP_LEVELS} + */ + getUserLevel(user) { + if (user.isGM) return CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER; + + const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); + const playerOwnership = this.ownership[user.id]; + return playerOwnership === undefined || playerOwnership === CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT + ? setting.defaultOwnership + : playerOwnership; + } + + async delete() { + const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); + const data = foundry.utils.deepClone(setting._source); + delete data.countdowns[this.id]; + await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data); + } } From 8d68166e4c643c26bc82882fd50773b4caf19e18 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Sun, 19 Jul 2026 12:42:16 +0200 Subject: [PATCH 7/8] Raised foundry compatability to 14.365 --- system.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system.json b/system.json index 214ab2ee..0aafe3f5 100644 --- a/system.json +++ b/system.json @@ -5,7 +5,7 @@ "version": "2.5.4", "compatibility": { "minimum": "14.364", - "verified": "14.364", + "verified": "14.365", "maximum": "14" }, "url": "https://github.com/Foundryborne/daggerheart", From 26bcc2dddc33dd6d4671ead3b1aef0881c6b1318 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:08:52 +0200 Subject: [PATCH 8/8] [Feature] Reload Check (#2051) --- lang/en.json | 20 +++++++++-- .../applications/sheets/actors/character.mjs | 15 ++++++-- module/applications/ui/chatLog.mjs | 9 +++++ module/config/settingsConfig.mjs | 15 ++++++++ module/data/action/attackAction.mjs | 21 ++++++++++++ module/data/chat-message/actorRoll.mjs | 1 + module/data/item/weapon.mjs | 8 +++++ module/data/settings/Automation.mjs | 7 ++++ module/dice/dhRoll.mjs | 21 ++++++++++-- ...lack_Powder_Revolver_AokqTusPzn0hghkE.json | 9 +++++ .../weapon_Blunderbuss_SLFrK0WmldPo0shz.json | 9 +++++ .../weapon_Hand_Cannon_MyGz8nd5sieRQ7zl.json | 9 +++++ ...eapon_Ilmari_s_Rifle_TMrUzVC3KvcHmdt8.json | 9 +++++ ...eapon_Magus_Revolver_jGykNGQiKm63tCiE.json | 9 +++++ styles/less/global/inventory-item.less | 4 +++ styles/less/ui/chat/ability-use.less | 5 +++ styles/less/ui/chat/action.less | 7 +++- styles/less/ui/chat/chat.less | 34 ++++++++++++++++--- .../settings/automation-settings/roll.hbs | 1 + .../sheets/global/partials/item-resource.hbs | 10 ++++-- templates/ui/chat/parts/button-part.hbs | 3 ++ templates/ui/chat/roll.hbs | 11 ++++++ 22 files changed, 223 insertions(+), 14 deletions(-) diff --git a/lang/en.json b/lang/en.json index b65cade7..d0b8d0f7 100755 --- a/lang/en.json +++ b/lang/en.json @@ -126,6 +126,10 @@ "damageOnSave": "Damage on Save", "useDefaultItemValues": "Use default Item values" }, + "Reload": { + "checkReload": "Check Reload", + "reloadRequired": "Reload Required!" + }, "RollField": { "diceRolling": { "compare": "Should be", @@ -1322,6 +1326,11 @@ "short": "V. Far" } }, + "ReloadChoices": { + "off": { "label": "Don't Use" }, + "button": { "label": "Use button" }, + "auto": { "label": "Automatic" } + }, "RollTypes": { "trait": { "name": "Trait" @@ -2221,7 +2230,9 @@ }, "Resource": { "single": "Resource", - "plural": "Resources" + "plural": "Resources", + "unloaded": "The weapon is not loaded", + "loaded": "The weapon is loaded" }, "Roll": { "attack": "Attack Roll", @@ -2797,6 +2808,10 @@ "hint": "Effects with defined range dependency will automatically turn on/off depending on range" } }, + "reload": { + "label": "Reload Checking", + "hint": "If the system should present a button for checking if the character will need to reload or do it automatically" + }, "resourceScrollTexts": { "label": "Show Resource Change Scrolltexts", "hint": "When a character is damaged, uses armor etc, a scrolling text will briefly appear by the token to signify this." @@ -3258,7 +3273,8 @@ "knowTheTide": "Know The Tide gained a token", "lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}", "noTokenTargeted": "No token is targeted", - "behaviorRegionRequiresGM": "Creating a Region with an attached Behavior requires an online GM" + "behaviorRegionRequiresGM": "Creating a Region with an attached Behavior requires an online GM", + "reloadRequired": "The {weapon} must be reloaded to be used!" }, "Progress": { "migrationLabel": "Performing system migration. Please wait and do not close Foundry." diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index b5c04f78..b88ec6fd 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -3,7 +3,7 @@ import DhDeathMove from '../../dialogs/deathMove.mjs'; import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs'; import DhCharacterCreation from '../../characterCreation/characterCreation.mjs'; import FilterMenu from '../../ux/filter-menu.mjs'; -import { getArmorSources, getDocFromElement, getDocFromElementSync, sortBy } from '../../../helpers/utils.mjs'; +import { getArmorSources, getDocFromElement, getDocFromElementSync, itemAbleRollParse, sortBy } from '../../../helpers/utils.mjs'; /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ @@ -29,6 +29,7 @@ export default class CharacterSheet extends DHBaseActorSheet { toggleResourceDice: CharacterSheet.#toggleResourceDice, handleResourceDice: CharacterSheet.#handleResourceDice, advanceResourceDie: CharacterSheet.#advanceResourceDie, + toggleItemReload: CharacterSheet.#onToggleItemReload, cancelBeastform: CharacterSheet.#cancelBeastform, toggleResourceManagement: CharacterSheet.#toggleResourceManagement, useDowntime: this.useDowntime, @@ -954,11 +955,21 @@ export default class CharacterSheet extends DHBaseActorSheet { }); } - /** */ static #advanceResourceDie(_, target) { this.updateResourceDie(target, true); } + static async #onToggleItemReload(_, target) { + const item = await getDocFromElement(target); + if (!item || !item.system.resource?.max) + return; + + await item.update({ + 'system.resource.value': item.system.needsReload ? + itemAbleRollParse(item.system.resource.max, this.document, item) : 0 + }) + } + lowerResourceDie(event) { event.preventDefault(); event.stopPropagation(); diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index c457a6a6..0ad436ec 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -152,6 +152,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo html.querySelectorAll('.risk-it-all-button').forEach(element => element.addEventListener('click', event => this.riskItAllClearStressAndHitPoints(event, data)) ); + for (const element of html.querySelectorAll('.roll-reload-check')) { + element.addEventListener('click', event => this.onRollReloadCheck(event, message)); + } }; setupHooks() { @@ -275,4 +278,10 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo const actor = game.actors.get(event.target.dataset.actorId); new game.system.api.applications.dialogs.RiskItAllDialog(actor, resourceValue).render({ force: true }); } + + async onRollReloadCheck(_event, messageData) { + const message = game.messages.get(messageData._id); + const needsReload = await message.system.action.handleReload?.({ awaitRoll: true }); + await message.update({ 'system.needsReload': needsReload }); + } } diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index 7aa1b0f7..daa89959 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -62,3 +62,18 @@ export const actionAutomationChoices = { label: 'DAGGERHEART.CONFIG.ActionAutomationChoices.always' } }; + +export const reloadChoices = { + off: { + id: 'off', + label: 'DAGGERHEART.CONFIG.ReloadChoices.off.label' + }, + button: { + id: 'button', + label: 'DAGGERHEART.CONFIG.ReloadChoices.button.label' + }, + auto: { + id: 'auto', + label: 'DAGGERHEART.CONFIG.ReloadChoices.auto.label' + } +}; \ No newline at end of file diff --git a/module/data/action/attackAction.mjs b/module/data/action/attackAction.mjs index dadc85f0..bf78520d 100644 --- a/module/data/action/attackAction.mjs +++ b/module/data/action/attackAction.mjs @@ -52,6 +52,10 @@ export default class DHAttackAction extends DHDamageAction { } async use(event, options) { + if (this.item?.system.needsReload) { + return ui.notifications.error(_loc('DAGGERHEART.UI.Notifications.reloadRequired', { weapon: this.item.name })); + } + const result = await super.use(event, options); if (result?.message?.system.action?.roll?.type === 'attack') { @@ -62,6 +66,23 @@ export default class DHAttackAction extends DHDamageAction { return result; } + async handleReload(options = { awaitRoll: false }) { + const roll = await new Roll('1d6').evaluate(); + if (game.modules.get('dice-so-nice')?.active) { + if (options.awaitRoll) + await game.dice3d.showForRoll(roll, game.user, true); + else + game.dice3d.showForRoll(roll, game.user, true); + } + + const needsToReload = roll.total === 1; + if (needsToReload) { + this.item.update({ 'system.resource.value': 0 }); + } + + return needsToReload; + } + /** * Generate a localized label array for this item subtype. * @returns {(string | { value: string, icons: string[] })[]} An array of localized strings and damage label objects. diff --git a/module/data/chat-message/actorRoll.mjs b/module/data/chat-message/actorRoll.mjs index dd81952b..7071ed1e 100644 --- a/module/data/chat-message/actorRoll.mjs +++ b/module/data/chat-message/actorRoll.mjs @@ -42,6 +42,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { hasEffect: new fields.BooleanField({ initial: false }), hasSave: new fields.BooleanField({ initial: false }), hasTarget: new fields.BooleanField({ initial: false }), + needsReload: new fields.BooleanField({ initial: false }), isDirect: new fields.BooleanField({ initial: false }), onSave: new fields.StringField(), source: new fields.SchemaField({ diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 5ff2d8d1..3d50e68a 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -116,6 +116,14 @@ export default class DHWeapon extends AttachableItem { return this.weaponFeatures; } + get hasReload() { + return Boolean(this.weaponFeatures.find(x => x.value === 'reloading')); + } + + get needsReload() { + return this.hasReload && this.resource.value === 0; + } + /**@inheritdoc */ async getDescriptionData() { const baseDescription = this.description; diff --git a/module/data/settings/Automation.mjs b/module/data/settings/Automation.mjs index 35e87327..e7bc0458 100644 --- a/module/data/settings/Automation.mjs +++ b/module/data/settings/Automation.mjs @@ -196,6 +196,13 @@ export default class DhAutomation extends foundry.abstract.DataModel { }) }) }), + reload: new fields.StringField({ + required: true, + choices: CONFIG.DH.SETTINGS.reloadChoices, + initial: CONFIG.DH.SETTINGS.reloadChoices.button.id, + label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.reload.label', + hint: 'DAGGERHEART.SETTINGS.Automation.FIELDS.reload.hint' + }), autoExpireActiveEffects: new fields.BooleanField({ required: true, initial: true, diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index c78caa4f..62d9ff9f 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -117,7 +117,11 @@ export default class DHRoll extends BaseRoll { static async toMessage(roll, config) { const item = config.data.parent?.items?.get?.(config.source.item) ?? null; - const action = item ? item.system.actions.get(config.source.action) : null; + const actions = item ? [ + ...item.system.actions, + ...(item.system.attack?.id === config.source.action ? [item.system.attack] : []) + ] : []; + const action = actions.find(x => x.id === config.source.action); let actionDescription = null; if (action?.chatDisplay) { actionDescription = action @@ -129,6 +133,14 @@ export default class DHRoll extends BaseRoll { config.actionChatMessageHandled = true; } + const reloadSetting = + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).reload; + const useReload = + item?.system.hasReload && + action?.type === 'attack' && + reloadSetting === CONFIG.DH.SETTINGS.reloadChoices.auto.id; + const needsReload = useReload ? await action?.handleReload?.() : false; + const cls = getDocumentClass('ChatMessage'), msgData = { type: this.messageType, @@ -136,7 +148,7 @@ export default class DHRoll extends BaseRoll { title: roll.title, speaker: cls.getSpeaker({ actor: roll.data?.parent }), sound: config.mute ? null : CONFIG.sounds.dice, - system: { ...config, actionDescription }, + system: { ...config, actionDescription, needsReload }, rolls: [roll] }; @@ -158,14 +170,17 @@ export default class DHRoll extends BaseRoll { if (!this._evaluated) return; const metagamingSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming); + const automationSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options }); return foundry.applications.handlebars.renderTemplate(template, { roll: this, ...chatData, + action: chatData.action, parent: chatData.parent, targetMode: chatData.targetMode, areas: chatData.action?.areas, - metagamingSettings + metagamingSettings, + automationSettings }); } diff --git a/src/packs/items/weapons/weapon_Black_Powder_Revolver_AokqTusPzn0hghkE.json b/src/packs/items/weapons/weapon_Black_Powder_Revolver_AokqTusPzn0hghkE.json index 34371c2b..f8b254f3 100644 --- a/src/packs/items/weapons/weapon_Black_Powder_Revolver_AokqTusPzn0hghkE.json +++ b/src/packs/items/weapons/weapon_Black_Powder_Revolver_AokqTusPzn0hghkE.json @@ -128,6 +128,15 @@ "source": "Daggerheart SRD", "page": 48, "artist": "" + }, + "resource": { + "type": "simple", + "value": 1, + "max": "1", + "recovery": null, + "progression": "decreasing", + "dieFaces": "d4", + "icon": "fa-solid fa-gun" } }, "effects": [], diff --git a/src/packs/items/weapons/weapon_Blunderbuss_SLFrK0WmldPo0shz.json b/src/packs/items/weapons/weapon_Blunderbuss_SLFrK0WmldPo0shz.json index 9b2f455a..570d8856 100644 --- a/src/packs/items/weapons/weapon_Blunderbuss_SLFrK0WmldPo0shz.json +++ b/src/packs/items/weapons/weapon_Blunderbuss_SLFrK0WmldPo0shz.json @@ -128,6 +128,15 @@ "source": "Daggerheart SRD", "page": 46, "artist": "" + }, + "resource": { + "type": "simple", + "value": 1, + "max": "1", + "recovery": null, + "progression": "decreasing", + "dieFaces": "d4", + "icon": "fa-solid fa-gun" } }, "effects": [], diff --git a/src/packs/items/weapons/weapon_Hand_Cannon_MyGz8nd5sieRQ7zl.json b/src/packs/items/weapons/weapon_Hand_Cannon_MyGz8nd5sieRQ7zl.json index 4967c6e4..f9d54bcd 100644 --- a/src/packs/items/weapons/weapon_Hand_Cannon_MyGz8nd5sieRQ7zl.json +++ b/src/packs/items/weapons/weapon_Hand_Cannon_MyGz8nd5sieRQ7zl.json @@ -128,6 +128,15 @@ "source": "Daggerheart SRD", "page": 50, "artist": "" + }, + "resource": { + "type": "simple", + "value": 1, + "max": "1", + "recovery": null, + "progression": "decreasing", + "dieFaces": "d4", + "icon": "fa-solid fa-gun" } }, "effects": [], diff --git a/src/packs/items/weapons/weapon_Ilmari_s_Rifle_TMrUzVC3KvcHmdt8.json b/src/packs/items/weapons/weapon_Ilmari_s_Rifle_TMrUzVC3KvcHmdt8.json index cddd762a..512168f5 100644 --- a/src/packs/items/weapons/weapon_Ilmari_s_Rifle_TMrUzVC3KvcHmdt8.json +++ b/src/packs/items/weapons/weapon_Ilmari_s_Rifle_TMrUzVC3KvcHmdt8.json @@ -128,6 +128,15 @@ "source": "Daggerheart SRD", "page": 49, "artist": "" + }, + "resource": { + "type": "simple", + "value": 1, + "max": "1", + "recovery": null, + "progression": "decreasing", + "dieFaces": "d4", + "icon": "fa-solid fa-gun" } }, "effects": [], diff --git a/src/packs/items/weapons/weapon_Magus_Revolver_jGykNGQiKm63tCiE.json b/src/packs/items/weapons/weapon_Magus_Revolver_jGykNGQiKm63tCiE.json index 9dbbb1c1..fd3e86d3 100644 --- a/src/packs/items/weapons/weapon_Magus_Revolver_jGykNGQiKm63tCiE.json +++ b/src/packs/items/weapons/weapon_Magus_Revolver_jGykNGQiKm63tCiE.json @@ -128,6 +128,15 @@ "source": "Daggerheart SRD", "page": 51, "artist": "" + }, + "resource": { + "type": "simple", + "value": 1, + "max": "1", + "recovery": null, + "progression": "decreasing", + "dieFaces": "d4", + "icon": "fa-solid fa-gun" } }, "effects": [], diff --git a/styles/less/global/inventory-item.less b/styles/less/global/inventory-item.less index d942133c..d2ca76c6 100644 --- a/styles/less/global/inventory-item.less +++ b/styles/less/global/inventory-item.less @@ -144,6 +144,10 @@ display: flex; align-items: center; gap: 4px; + + .unloaded { + opacity: 0.5; + } } } diff --git a/styles/less/ui/chat/ability-use.less b/styles/less/ui/chat/ability-use.less index c31136ad..9d3cb84e 100644 --- a/styles/less/ui/chat/ability-use.less +++ b/styles/less/ui/chat/ability-use.less @@ -133,6 +133,11 @@ height: 40px; flex: 1 1 calc(50% - 5px); + span { + font-family: @font-body; + font-weight: 700; + } + &:nth-last-child(1):nth-child(odd) { flex-basis: 100%; } diff --git a/styles/less/ui/chat/action.less b/styles/less/ui/chat/action.less index 6eeb7a52..cff617d8 100644 --- a/styles/less/ui/chat/action.less +++ b/styles/less/ui/chat/action.less @@ -46,9 +46,14 @@ padding: 0 8px; button { - height: 40px; + height: 36px; flex: 1 1 calc(50% - 5px); + span { + font-family: @font-body; + font-weight: 700; + } + &:nth-last-child(1):nth-child(odd) { flex-basis: 100%; } diff --git a/styles/less/ui/chat/chat.less b/styles/less/ui/chat/chat.less index 4d627e39..8c3f4b08 100644 --- a/styles/less/ui/chat/chat.less +++ b/styles/less/ui/chat/chat.less @@ -271,6 +271,28 @@ } } + .roll-reload-container { + text-align: center; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + + .reload-warning { + display: flex; + align-items: center; + border-radius: 5px; + width: fit-content; + gap: 5px; + cursor: pointer; + padding: 5px; + transition: all 0.3s ease; + color: @beige; + background: @red-40; + outline: 1px solid @red; + } + } + .roll-part-extra { display: flex; justify-content: center; @@ -622,15 +644,19 @@ .roll-buttons { display: flex; + flex-wrap: wrap; gap: 5px; + width: 100%; margin-top: 8px; button { - height: 32px; - flex: 1; + height: 36px; + flex: 1 1 calc(50% - 5px); + font-family: @font-body; + font-weight: 700; - &.end-button { - flex: 0; + &:nth-last-child(1):nth-child(odd) { + flex-basis: 100%; } } } diff --git a/templates/settings/automation-settings/roll.hbs b/templates/settings/automation-settings/roll.hbs index dc65f8ae..a0b19ee4 100644 --- a/templates/settings/automation-settings/roll.hbs +++ b/templates/settings/automation-settings/roll.hbs @@ -18,6 +18,7 @@

    {{localize (concat "DAGGERHEART.SETTINGS.Automation.FIELDS.roll." field.name ".hint")}}

    {{/each}} + {{formGroup settingFields.schema.fields.reload value=settingFields.reload localize=true}}
    diff --git a/templates/sheets/global/partials/item-resource.hbs b/templates/sheets/global/partials/item-resource.hbs index fbcf02ca..cde10d37 100644 --- a/templates/sheets/global/partials/item-resource.hbs +++ b/templates/sheets/global/partials/item-resource.hbs @@ -1,7 +1,13 @@ {{#if (eq item.system.resource.type 'simple')}}
    - - + {{#if item.system.hasReload}} + + + + {{else}} + + + {{/if}}
    {{else if (eq item.system.resource.type 'diceValue')}}
    diff --git a/templates/ui/chat/parts/button-part.hbs b/templates/ui/chat/parts/button-part.hbs index a0075b5f..b631192c 100644 --- a/templates/ui/chat/parts/button-part.hbs +++ b/templates/ui/chat/parts/button-part.hbs @@ -1,4 +1,7 @@
    + {{#if (eq automationSettings.reload 'button')}} + + {{/if}} {{#if areas.length}}{{/if}} {{#if hasDamage}} {{#if damage.active}} diff --git a/templates/ui/chat/roll.hbs b/templates/ui/chat/roll.hbs index c7b17b21..ab4e197e 100644 --- a/templates/ui/chat/roll.hbs +++ b/templates/ui/chat/roll.hbs @@ -1,5 +1,16 @@
    {{title}}
    + + {{#if (eq action.type 'attack')}} + {{#if needsReload}} +
    + {{#if needsReload}} +

    {{localize "DAGGERHEART.ACTIONS.Reload.reloadRequired"}}

    + {{/if}} +
    + {{/if}} + {{/if}} + {{#if actionDescription}}{{> 'systems/daggerheart/templates/ui/chat/parts/description-part.hbs'}}{{/if}} {{#if hasRoll}}
    {{localize "Result"}}