From c5308617e1880eba81d76b8e932fac169ac0bce6 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Sat, 27 Jun 2026 22:31:36 +0200 Subject: [PATCH 01/26] Moved v13 countdown migration over to a migrateData --- module/data/countdowns.mjs | 33 ++++++++++++++++++++ module/systemRegistration/migrations.mjs | 38 ------------------------ 2 files changed, 33 insertions(+), 38 deletions(-) diff --git a/module/data/countdowns.mjs b/module/data/countdowns.mjs index 8e55ed31..ffe4d26b 100644 --- a/module/data/countdowns.mjs +++ b/module/data/countdowns.mjs @@ -28,6 +28,39 @@ export default class DhCountdowns extends foundry.abstract.DataModel { for (const countdownKey of changedCountdowns) foundry.ui.countdowns.changedCountdownsForAnimation.add(countdownKey); } + + static migrateData(source) { + const migrateOldCountdowns = (data, type) => { + for (const key of Object.keys(data.countdowns)) { + const countdown = data.countdowns[key]; + source.countdowns[key] = { + ...countdown, + type: type, + ownership: Object.keys(countdown.ownership.players).reduce((acc, key) => { + acc[key] = + countdown.ownership.players[key].type === 1 ? 2 : countdown.ownership.players[key].type; + return acc; + }, {}), + progress: { + ...countdown.progress, + type: countdown.progress.type.value + } + }; + } + + source[type] = null; + }; + + if (source.narrative) { + migrateOldCountdowns(source.narrative, 'narrative'); + } + + if (source.encounter) { + migrateOldCountdowns(source.encounter, 'encounter'); + } + + return super.migrateData(source); + } } export class DhCountdown extends foundry.abstract.DataModel { diff --git a/module/systemRegistration/migrations.mjs b/module/systemRegistration/migrations.mjs index ec546c92..a77c6d8b 100644 --- a/module/systemRegistration/migrations.mjs +++ b/module/systemRegistration/migrations.mjs @@ -1,5 +1,4 @@ import { defaultRestOptions } from '../config/generalConfig.mjs'; -import { RefreshType, socketEvent } from './socket.mjs'; export async function runMigrations() { let lastMigrationVersion = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion); @@ -153,43 +152,6 @@ export async function runMigrations() { await pack.configure({ locked: true }); } - /* Migrate old countdown structure */ - const countdownSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); - const getCountdowns = (data, type) => { - return Object.keys(data.countdowns).reduce((acc, key) => { - const countdown = data.countdowns[key]; - acc[key] = { - ...countdown, - type: type, - ownership: Object.keys(countdown.ownership.players).reduce((acc, key) => { - acc[key] = - countdown.ownership.players[key].type === 1 ? 2 : countdown.ownership.players[key].type; - return acc; - }, {}), - progress: { - ...countdown.progress, - type: countdown.progress.type.value - } - }; - - return acc; - }, {}); - }; - - await countdownSettings.updateSource({ - countdowns: { - ...getCountdowns(countdownSettings.narrative, 'narrative'), - ...getCountdowns(countdownSettings.encounter, 'encounter') - } - }); - await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, countdownSettings); - - game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.Refresh, - data: { refreshType: RefreshType.Countdown } - }); - Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown }); - lastMigrationVersion = '1.2.0'; } From f02e97f0cdf6483c63e33aeb11ef915e69b55c72 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Sat, 27 Jun 2026 23:10:33 +0200 Subject: [PATCH 02/26] Revert "Moved v13 countdown migration over to a migrateData" This reverts commit c5308617e1880eba81d76b8e932fac169ac0bce6. --- module/data/countdowns.mjs | 33 -------------------- module/systemRegistration/migrations.mjs | 38 ++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/module/data/countdowns.mjs b/module/data/countdowns.mjs index ffe4d26b..8e55ed31 100644 --- a/module/data/countdowns.mjs +++ b/module/data/countdowns.mjs @@ -28,39 +28,6 @@ export default class DhCountdowns extends foundry.abstract.DataModel { for (const countdownKey of changedCountdowns) foundry.ui.countdowns.changedCountdownsForAnimation.add(countdownKey); } - - static migrateData(source) { - const migrateOldCountdowns = (data, type) => { - for (const key of Object.keys(data.countdowns)) { - const countdown = data.countdowns[key]; - source.countdowns[key] = { - ...countdown, - type: type, - ownership: Object.keys(countdown.ownership.players).reduce((acc, key) => { - acc[key] = - countdown.ownership.players[key].type === 1 ? 2 : countdown.ownership.players[key].type; - return acc; - }, {}), - progress: { - ...countdown.progress, - type: countdown.progress.type.value - } - }; - } - - source[type] = null; - }; - - if (source.narrative) { - migrateOldCountdowns(source.narrative, 'narrative'); - } - - if (source.encounter) { - migrateOldCountdowns(source.encounter, 'encounter'); - } - - return super.migrateData(source); - } } export class DhCountdown extends foundry.abstract.DataModel { diff --git a/module/systemRegistration/migrations.mjs b/module/systemRegistration/migrations.mjs index a77c6d8b..ec546c92 100644 --- a/module/systemRegistration/migrations.mjs +++ b/module/systemRegistration/migrations.mjs @@ -1,4 +1,5 @@ import { defaultRestOptions } from '../config/generalConfig.mjs'; +import { RefreshType, socketEvent } from './socket.mjs'; export async function runMigrations() { let lastMigrationVersion = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion); @@ -152,6 +153,43 @@ export async function runMigrations() { await pack.configure({ locked: true }); } + /* Migrate old countdown structure */ + const countdownSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); + const getCountdowns = (data, type) => { + return Object.keys(data.countdowns).reduce((acc, key) => { + const countdown = data.countdowns[key]; + acc[key] = { + ...countdown, + type: type, + ownership: Object.keys(countdown.ownership.players).reduce((acc, key) => { + acc[key] = + countdown.ownership.players[key].type === 1 ? 2 : countdown.ownership.players[key].type; + return acc; + }, {}), + progress: { + ...countdown.progress, + type: countdown.progress.type.value + } + }; + + return acc; + }, {}); + }; + + await countdownSettings.updateSource({ + countdowns: { + ...getCountdowns(countdownSettings.narrative, 'narrative'), + ...getCountdowns(countdownSettings.encounter, 'encounter') + } + }); + await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, countdownSettings); + + game.socket.emit(`system.${CONFIG.DH.id}`, { + action: socketEvent.Refresh, + data: { refreshType: RefreshType.Countdown } + }); + Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown }); + lastMigrationVersion = '1.2.0'; } From 7409a577f6ab2c3b8bf64380f8548ded1fdab961 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Fri, 3 Jul 2026 00:58:48 -0400 Subject: [PATCH 03/26] Remove certain fieldsets and adjust scrollsbars --- .../applications/sheets/actors/adversary.mjs | 2 +- styles/less/global/elements.less | 2 +- styles/less/global/sheet.less | 2 +- .../sheets/actors/actor-sheet-shared.less | 17 +- .../less/sheets/actors/adversary/effects.less | 5 +- .../sheets/actors/adversary/features.less | 8 +- .../less/sheets/actors/adversary/sheet.less | 2 +- .../sheets/actors/character/biography.less | 11 +- .../less/sheets/actors/character/effects.less | 5 +- .../sheets/actors/character/features.less | 5 +- .../less/sheets/actors/character/header.less | 19 +- .../less/sheets/actors/character/index.less | 2 +- .../sheets/actors/character/inventory.less | 8 +- .../less/sheets/actors/character/loadout.less | 7 +- .../less/sheets/actors/character/sheet.less | 2 +- .../less/sheets/actors/companion/details.less | 5 +- .../less/sheets/actors/companion/effects.less | 5 +- .../sheets/actors/environment/features.less | 8 +- .../less/sheets/actors/environment/index.less | 2 +- .../environment/potentialAdversaries.less | 5 +- .../less/sheets/actors/environment/sheet.less | 2 - .../sheets/actors/party/party-members.less | 3 +- styles/less/sheets/actors/party/sheet.less | 13 +- styles/less/utils/mixin.less | 10 +- .../sheets/actors/adversary/features.hbs | 21 +- .../sheets/actors/character/features.hbs | 50 +-- templates/sheets/actors/character/header.hbs | 326 +++++++++--------- .../sheets/actors/environment/features.hbs | 21 +- 28 files changed, 283 insertions(+), 285 deletions(-) diff --git a/module/applications/sheets/actors/adversary.mjs b/module/applications/sheets/actors/adversary.mjs index bcfe3cbb..b6e17ddd 100644 --- a/module/applications/sheets/actors/adversary.mjs +++ b/module/applications/sheets/actors/adversary.mjs @@ -7,7 +7,7 @@ export default class AdversarySheet extends DHBaseActorSheet { /** @inheritDoc */ static DEFAULT_OPTIONS = { classes: ['adversary'], - position: { width: 645, height: 760 }, + position: { width: 645, height: 750 }, window: { resizable: true }, actions: { toggleHitPoints: AdversarySheet.#toggleHitPoints, diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index f7934b71..d570c08c 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -261,7 +261,7 @@ fieldset { align-items: center; - margin-top: 5px; + margin: 5px 0 0 0; border-radius: 6px; border-color: @color-fieldset-border; padding-inline: 0.625rem; diff --git a/styles/less/global/sheet.less b/styles/less/global/sheet.less index e3072da1..8381c7c3 100755 --- a/styles/less/global/sheet.less +++ b/styles/less/global/sheet.less @@ -54,7 +54,7 @@ body.game:is(.performance-low, .noblur) { position: relative; min-height: -webkit-fill-available; transition: opacity 0.3s ease; - padding-bottom: 20px; + padding-bottom: 16px; .tab { padding: 0 10px; diff --git a/styles/less/sheets/actors/actor-sheet-shared.less b/styles/less/sheets/actors/actor-sheet-shared.less index 5eb5b43c..37e8579f 100644 --- a/styles/less/sheets/actors/actor-sheet-shared.less +++ b/styles/less/sheets/actors/actor-sheet-shared.less @@ -39,6 +39,20 @@ .window-header > .attribution-header-label { margin-right: var(--spacer-4); + pointer-events: none; + } + + .tab-navigation { + margin-bottom: 0; + } + + .tab { + flex: 1; + padding: 0; + overflow: hidden; + .search-section { + padding: 12px 14px var(--spacer-8) 12px; + } } .tab.inventory { @@ -46,7 +60,7 @@ display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 10px; - padding: 10px 10px 0; + padding: var(--spacer-8) 16px var(--spacer-8) 16px; .input { color: light-dark(@dark, @beige); @@ -57,7 +71,6 @@ .tab.notes.active { padding: 0; margin: 0; - margin-top: -10px; // will be removed once tab-navigation bottom margin is removed on all actor sheets scrollbar-gutter: unset; // Add padding around top level level prosemirrors used for note tabs diff --git a/styles/less/sheets/actors/adversary/effects.less b/styles/less/sheets/actors/adversary/effects.less index 4aa44e51..f489bee6 100644 --- a/styles/less/sheets/actors/adversary/effects.less +++ b/styles/less/sheets/actors/adversary/effects.less @@ -7,9 +7,8 @@ display: flex; flex-direction: column; gap: 10px; - overflow-y: auto; - padding-bottom: 20px; - .with-scroll-shadows(); + padding: 7px calc(12px - var(--scrollbar-width)) 4px 12px; + .stable-scroll-container(); } } } diff --git a/styles/less/sheets/actors/adversary/features.less b/styles/less/sheets/actors/adversary/features.less index 447d050e..49289d36 100644 --- a/styles/less/sheets/actors/adversary/features.less +++ b/styles/less/sheets/actors/adversary/features.less @@ -5,12 +5,8 @@ .application.sheet.daggerheart.actor.dh-style.adversary { .tab.features { .feature-section { - display: flex; - flex-direction: column; - gap: 10px; - overflow-y: auto; - padding-bottom: 20px; - .with-scroll-shadows(); + padding: 16px calc(16px - var(--scrollbar-width)) 4px 16px; + .stable-scroll-container(); } } } diff --git a/styles/less/sheets/actors/adversary/sheet.less b/styles/less/sheets/actors/adversary/sheet.less index 639af98b..1eb7d423 100644 --- a/styles/less/sheets/actors/adversary/sheet.less +++ b/styles/less/sheets/actors/adversary/sheet.less @@ -30,9 +30,9 @@ grid-row: 2; grid-column: 2; &.active { - overflow: hidden; display: flex; flex-direction: column; + margin: 0 0 10px 0; } } } \ No newline at end of file diff --git a/styles/less/sheets/actors/character/biography.less b/styles/less/sheets/actors/character/biography.less index 8548a2fb..9782a588 100644 --- a/styles/less/sheets/actors/character/biography.less +++ b/styles/less/sheets/actors/character/biography.less @@ -8,17 +8,14 @@ display: flex; flex-direction: column; gap: 10px; - height: 100%; - overflow-y: auto; - padding-top: 8px; - padding-bottom: 20px; - height: 100%; - .with-scroll-shadows(); + height: 100%; + padding: 12px calc(12px - var(--scrollbar-width)) 4px 12px; + .stable-scroll-container(); } .characteristics-section { gap: 20px; - padding: 0 10px; + padding: 0 4px; } .biography-section { diff --git a/styles/less/sheets/actors/character/effects.less b/styles/less/sheets/actors/character/effects.less index 0ab1007d..8fac301c 100644 --- a/styles/less/sheets/actors/character/effects.less +++ b/styles/less/sheets/actors/character/effects.less @@ -8,9 +8,8 @@ display: flex; flex-direction: column; gap: 10px; - overflow-y: auto; - padding-bottom: 20px; - .with-scroll-shadows(); + padding: 7px calc(12px - var(--scrollbar-width)) 4px 12px; + .stable-scroll-container(); } } } diff --git a/styles/less/sheets/actors/character/features.less b/styles/less/sheets/actors/character/features.less index 52b41826..fcde3e38 100644 --- a/styles/less/sheets/actors/character/features.less +++ b/styles/less/sheets/actors/character/features.less @@ -8,9 +8,8 @@ display: flex; flex-direction: column; gap: 10px; - overflow-y: auto; - padding-bottom: 20px; - .with-scroll-shadows(); + padding: 7px calc(12px - var(--scrollbar-width)) 4px 12px; + .stable-scroll-container(); } } } diff --git a/styles/less/sheets/actors/character/header.less b/styles/less/sheets/actors/character/header.less index 91b3545a..81345715 100644 --- a/styles/less/sheets/actors/character/header.less +++ b/styles/less/sheets/actors/character/header.less @@ -19,16 +19,19 @@ .application.sheet.daggerheart.actor.dh-style.character { .character-header-sheet { - padding: 0 15px; padding-top: var(--header-height); width: 100%; + > *:not(line-div, .tab-navigation) { + padding-left: 15px; + padding-right: 15px; + } + .name-row { display: flex; gap: 6px; align-items: start; justify-content: space-between; - padding: 0; padding-top: 5px; flex: 1; @@ -100,8 +103,8 @@ .character-details { display: flex; justify-content: space-between; - padding: 5px 0; - margin-bottom: 8px; + margin-top: 5px; + margin-bottom: 10px; font-size: var(--font-size-12); color: @color-text-emphatic; @@ -130,7 +133,6 @@ .character-row { display: flex; align-items: center; - padding: 0; margin-bottom: 12px; .resource-section { @@ -218,12 +220,11 @@ .character-traits { display: flex; - padding: 0; margin-bottom: 15px; justify-content: space-between; max-width: 38.5rem; gap: 0.5rem; - padding-left: 0.5rem; + margin-left: 0.5rem; .trait { cursor: pointer; @@ -325,5 +326,9 @@ } } } + + .tab-navigation button[data-action="openSettings"] { + margin-right: 12px; + } } } diff --git a/styles/less/sheets/actors/character/index.less b/styles/less/sheets/actors/character/index.less index edefe0a1..f196d5bf 100644 --- a/styles/less/sheets/actors/character/index.less +++ b/styles/less/sheets/actors/character/index.less @@ -1,8 +1,8 @@ +@import './sheet.less'; @import './biography.less'; @import './effects.less'; @import './features.less'; @import './header.less'; @import './inventory.less'; @import './loadout.less'; -@import './sheet.less'; @import './sidebar.less'; diff --git a/styles/less/sheets/actors/character/inventory.less b/styles/less/sheets/actors/character/inventory.less index fcfbbee9..ce7a8cdb 100644 --- a/styles/less/sheets/actors/character/inventory.less +++ b/styles/less/sheets/actors/character/inventory.less @@ -7,11 +7,9 @@ .items-section { display: flex; flex-direction: column; - gap: 10px; - overflow-y: auto; - margin-top: 20px; - padding-bottom: 20px; - .with-scroll-shadows(); + gap: 10px; + padding: 8px calc(12px - var(--scrollbar-width)) 4px 12px; + .stable-scroll-container(); } } } diff --git a/styles/less/sheets/actors/character/loadout.less b/styles/less/sheets/actors/character/loadout.less index fa3e0176..cb1baaa6 100644 --- a/styles/less/sheets/actors/character/loadout.less +++ b/styles/less/sheets/actors/character/loadout.less @@ -50,11 +50,8 @@ display: flex; flex-direction: column; gap: 10px; - height: 100%; - overflow-y: auto; - margin-top: 20px; - padding-bottom: 20px; - .with-scroll-shadows(); + padding: 8px calc(12px - var(--scrollbar-width)) 4px 12px; + .stable-scroll-container(); } } } diff --git a/styles/less/sheets/actors/character/sheet.less b/styles/less/sheets/actors/character/sheet.less index 68792c99..43401b19 100644 --- a/styles/less/sheets/actors/character/sheet.less +++ b/styles/less/sheets/actors/character/sheet.less @@ -30,7 +30,7 @@ &.active { display: flex; flex-direction: column; - overflow: hidden; + margin: 0 0 10px 0; } } } diff --git a/styles/less/sheets/actors/companion/details.less b/styles/less/sheets/actors/companion/details.less index 2e43cac4..e10e7680 100644 --- a/styles/less/sheets/actors/companion/details.less +++ b/styles/less/sheets/actors/companion/details.less @@ -1,7 +1,10 @@ @import '../../../utils/colors.less'; @import '../../../utils/fonts.less'; -.application.sheet.daggerheart.actor.dh-style.companion { +.application.sheet.daggerheart.actor.dh-style.companion .tab.details.active { + padding: 12px calc(12px - var(--scrollbar-width)) 4px 12px; + .stable-scroll-container(); + .partner-section, .attack-section, .experience-list { diff --git a/styles/less/sheets/actors/companion/effects.less b/styles/less/sheets/actors/companion/effects.less index c0cac669..ffe66da8 100644 --- a/styles/less/sheets/actors/companion/effects.less +++ b/styles/less/sheets/actors/companion/effects.less @@ -6,9 +6,8 @@ display: flex; flex-direction: column; gap: 10px; - overflow-y: auto; - padding-bottom: 20px; - .with-scroll-shadows(); + padding: 7px calc(12px - var(--scrollbar-width)) 4px 12px; + .stable-scroll-container(); } } } diff --git a/styles/less/sheets/actors/environment/features.less b/styles/less/sheets/actors/environment/features.less index 84cf26f8..c6fe3750 100644 --- a/styles/less/sheets/actors/environment/features.less +++ b/styles/less/sheets/actors/environment/features.less @@ -5,12 +5,8 @@ .application.sheet.daggerheart.actor.dh-style.environment { .tab.features { .feature-section { - display: flex; - flex-direction: column; - gap: 10px; - overflow-y: auto; - padding-bottom: 4px; - .with-scroll-shadows(); + padding: 16px calc(16px - var(--scrollbar-width)) 4px 16px; + .stable-scroll-container(); } } } diff --git a/styles/less/sheets/actors/environment/index.less b/styles/less/sheets/actors/environment/index.less index 211c8e60..df7e6fc2 100644 --- a/styles/less/sheets/actors/environment/index.less +++ b/styles/less/sheets/actors/environment/index.less @@ -1,4 +1,4 @@ +@import './sheet.less'; @import './features.less'; @import './header.less'; @import './potentialAdversaries.less'; -@import './sheet.less'; diff --git a/styles/less/sheets/actors/environment/potentialAdversaries.less b/styles/less/sheets/actors/environment/potentialAdversaries.less index f112c0d2..07dc5f92 100644 --- a/styles/less/sheets/actors/environment/potentialAdversaries.less +++ b/styles/less/sheets/actors/environment/potentialAdversaries.less @@ -6,9 +6,8 @@ display: flex; flex-direction: column; gap: 10px; - overflow-y: auto; - padding-bottom: 4px; - .with-scroll-shadows(); + padding: 7px calc(12px - var(--scrollbar-width)) 4px 12px; + .stable-scroll-container(); } } } diff --git a/styles/less/sheets/actors/environment/sheet.less b/styles/less/sheets/actors/environment/sheet.less index 2d9cc188..ef9d542a 100644 --- a/styles/less/sheets/actors/environment/sheet.less +++ b/styles/less/sheets/actors/environment/sheet.less @@ -14,9 +14,7 @@ .application.sheet.daggerheart.actor.dh-style.environment { .tab { - flex: 1; overflow-y: auto; - &.active { overflow: hidden; display: flex; diff --git a/styles/less/sheets/actors/party/party-members.less b/styles/less/sheets/actors/party/party-members.less index 3d882345..ca384322 100644 --- a/styles/less/sheets/actors/party/party-members.less +++ b/styles/less/sheets/actors/party/party-members.less @@ -3,7 +3,8 @@ @import '../../../utils/mixin.less'; .application.sheet.daggerheart.actor.dh-style.party .tab.partyMembers { - overflow: auto; + padding: 12px calc(12px - var(--scrollbar-width)) 4px 12px; + .stable-scroll-container(); .actions-section { display: flex; diff --git a/styles/less/sheets/actors/party/sheet.less b/styles/less/sheets/actors/party/sheet.less index d24c712c..bf589696 100644 --- a/styles/less/sheets/actors/party/sheet.less +++ b/styles/less/sheets/actors/party/sheet.less @@ -17,15 +17,8 @@ }); .application.sheet.daggerheart.actor.dh-style.party { - .tab { - flex: 1; - overflow-y: auto; - scrollbar-gutter: stable; - - &.active { - overflow: auto; - display: flex; - flex-direction: column; - } + .tab.active { + display: flex; + flex-direction: column; } } diff --git a/styles/less/utils/mixin.less b/styles/less/utils/mixin.less index e2ef85ef..2ce85166 100644 --- a/styles/less/utils/mixin.less +++ b/styles/less/utils/mixin.less @@ -174,10 +174,10 @@ --fade-start: 0; } 10%, 100% { - --fade-start: 12px; + --fade-start: 14px; } 0%, 90% { - --fade-end: 12px; + --fade-end: 14px; } 100% { --fade-end: 0; @@ -198,3 +198,9 @@ transparent 100% ); } + +.stable-scroll-container() { + overflow-y: auto; + scrollbar-gutter: stable; + .with-scroll-shadows(); +} \ No newline at end of file diff --git a/templates/sheets/actors/adversary/features.hbs b/templates/sheets/actors/adversary/features.hbs index 3b495e74..05e95737 100644 --- a/templates/sheets/actors/adversary/features.hbs +++ b/templates/sheets/actors/adversary/features.hbs @@ -1,14 +1,15 @@
-
- {{> 'daggerheart.inventory-items' - title=tabs.features.label - type='feature' - collection=@root.features - hideContextMenu=true - hideModifyControls=true - canCreate=@root.editable - showActions=@root.editable - }} +
+ {{#each @root.features as |item|}} + {{> "daggerheart.inventory-item" + item=item + type="feature" + actorType=@root.document.type + hideContextMenu=true + hideModifyControls=true + showActions=@root.editable + }} + {{/each}}
\ No newline at end of file diff --git a/templates/sheets/actors/character/features.hbs b/templates/sheets/actors/character/features.hbs index b2760900..c96ab623 100644 --- a/templates/sheets/actors/character/features.hbs +++ b/templates/sheets/actors/character/features.hbs @@ -1,26 +1,26 @@ -
-
- {{#each document.system.sheetLists as |category|}} - {{#if (eq category.type 'feature' )}} - {{> 'daggerheart.inventory-items' - title=category.title - type='feature' - actorType='character' - collection=category.values - canCreate=@root.editable - showActions=@root.editable - }} - {{else if category.values}} - {{> 'daggerheart.inventory-items' - title=category.title - type='feature' - actorType='character' - collection=category.values - canCreate=false - showActions=@root.editable - }} - {{/if}} - {{/each}} -
+
+
+ {{#each document.system.sheetLists as |category|}} + {{#if (eq category.type 'feature' )}} + {{> 'daggerheart.inventory-items' + title=category.title + type='feature' + actorType='character' + collection=category.values + canCreate=@root.editable + showActions=@root.editable + }} + {{else if category.values}} + {{> 'daggerheart.inventory-items' + title=category.title + type='feature' + actorType='character' + collection=category.values + canCreate=false + showActions=@root.editable + }} + {{/if}} + {{/each}} +
\ No newline at end of file diff --git a/templates/sheets/actors/character/header.hbs b/templates/sheets/actors/character/header.hbs index 459911af..a40c336a 100644 --- a/templates/sheets/actors/character/header.hbs +++ b/templates/sheets/actors/character/header.hbs @@ -1,165 +1,163 @@ -
- -
-

{{source.name}}

-
-

- {{#if @root.editable}} - {{#if document.system.needsCharacterSetup}} - - {{else if document.system.levelData.canLevelUp}} - - {{/if}} - {{/if}} - {{#unless document.system.needsCharacterSetup}} - {{localize 'DAGGERHEART.GENERAL.level'}} - - {{/unless}} -

-
-
-
-
- {{#if document.system.class.value}} - {{document.system.class.value.name}} - {{else}} - {{localize 'TYPES.Item.class'}} - {{/if}} - - {{#if document.system.class.subclass}} - {{document.system.class.subclass.name}} - {{else}} - {{localize 'TYPES.Item.subclass'}} - {{/if}} - - {{#if document.system.community}} - {{document.system.community.name}} - {{else}} - {{localize 'TYPES.Item.community'}} - {{/if}} - - {{#if document.system.ancestry}} - {{document.system.ancestry.name}} - {{else}} - {{localize 'TYPES.Item.ancestry'}} - {{/if}} -
- - {{#if (or document.system.multiclass.value document.system.multiclass.subclass)}} -
- {{#if document.system.multiclass.value}} - {{document.system.multiclass.value.name}} - {{else}} - {{localize 'DAGGERHEART.GENERAL.multiclass'}} - {{/if}} - - {{#if document.system.multiclass.subclass}} - {{document.system.multiclass.subclass.name}} - {{else}} - {{localize 'TYPES.Item.subclass'}} - {{/if}} -
- {{/if}} - - -
- -
-
-
-

{{localize "DAGGERHEART.GENERAL.hope"}}

- {{#times document.system.resources.hope.max}} - - {{#if (gte ../document.system.resources.hope.value (add this 1))}} - - {{else}} - - {{/if}} - - {{/times}} - {{#times document.system.scars}} - - - - {{/times}} - {{#if hasExtraResources}}{{/if}} -
-
- {{#if document.system.class.value}} -
- {{#each document.system.domainData as |data|}} -
- -
- {{/each}} -
- {{/if}} -
- {{#if document.parties.size}} - - {{/if}} - {{#if @root.editable}} - - - {{/if}} -
-
- -
- {{#each this.attributes as |attribute key|}} -
-
-
- {{attribute.label}} -
-
- - - - -
- {{#if (gt attribute.value 0)}} - +{{attribute.value}} - {{else}} - {{attribute.value}} - {{/if}} -
- {{#if isSpellcasting}} -
- -
- {{/if}} -
-
- {{/each}} -
- - {{#> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }} - - {{/'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}} +
+ +
+

{{source.name}}

+
+

+ {{#if @root.editable}} + {{#if document.system.needsCharacterSetup}} + + {{else if document.system.levelData.canLevelUp}} + + {{/if}} + {{/if}} + {{#unless document.system.needsCharacterSetup}} + {{localize 'DAGGERHEART.GENERAL.level'}} + + {{/unless}} +

+
+
+
+
+ {{#if document.system.class.value}} + {{document.system.class.value.name}} + {{else}} + {{localize 'TYPES.Item.class'}} + {{/if}} + + {{#if document.system.class.subclass}} + {{document.system.class.subclass.name}} + {{else}} + {{localize 'TYPES.Item.subclass'}} + {{/if}} + + {{#if document.system.community}} + {{document.system.community.name}} + {{else}} + {{localize 'TYPES.Item.community'}} + {{/if}} + + {{#if document.system.ancestry}} + {{document.system.ancestry.name}} + {{else}} + {{localize 'TYPES.Item.ancestry'}} + {{/if}} +
+ + {{#if (or document.system.multiclass.value document.system.multiclass.subclass)}} +
+ {{#if document.system.multiclass.value}} + {{document.system.multiclass.value.name}} + {{else}} + {{localize 'DAGGERHEART.GENERAL.multiclass'}} + {{/if}} + + {{#if document.system.multiclass.subclass}} + {{document.system.multiclass.subclass.name}} + {{else}} + {{localize 'TYPES.Item.subclass'}} + {{/if}} +
+ {{/if}} +
+ +
+
+
+

{{localize "DAGGERHEART.GENERAL.hope"}}

+ {{#times document.system.resources.hope.max}} + + {{#if (gte ../document.system.resources.hope.value (add this 1))}} + + {{else}} + + {{/if}} + + {{/times}} + {{#times document.system.scars}} + + + + {{/times}} + {{#if hasExtraResources}}{{/if}} +
+
+ {{#if document.system.class.value}} +
+ {{#each document.system.domainData as |data|}} +
+ +
+ {{/each}} +
+ {{/if}} +
+ {{#if document.parties.size}} + + {{/if}} + {{#if @root.editable}} + + + {{/if}} +
+
+ +
+ {{#each this.attributes as |attribute key|}} +
+
+
+ {{attribute.label}} +
+
+ + + + +
+ {{#if (gt attribute.value 0)}} + +{{attribute.value}} + {{else}} + {{attribute.value}} + {{/if}} +
+ {{#if isSpellcasting}} +
+ +
+ {{/if}} +
+
+ {{/each}} +
+ + {{#> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }} + + {{/'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
\ No newline at end of file diff --git a/templates/sheets/actors/environment/features.hbs b/templates/sheets/actors/environment/features.hbs index 35fcb038..85fa7066 100644 --- a/templates/sheets/actors/environment/features.hbs +++ b/templates/sheets/actors/environment/features.hbs @@ -3,15 +3,16 @@ data-tab='{{tabs.features.id}}' data-group='{{tabs.features.group}}' > -
- {{> 'daggerheart.inventory-items' - title=tabs.features.label - type='feature' - collection=@root.features - hideContextMenu=true - hideModifyControls=true - canCreate=@root.editable - showActions=@root.editable - }} +
+ {{#each @root.features as |item|}} + {{> "daggerheart.inventory-item" + item=item + type="feature" + actorType=@root.document.type + hideContextMenu=true + hideModifyControls=true + showActions=@root.editable + }} + {{/each}}
\ No newline at end of file From b73c70aeb842ab2bdc03a12d21451cb9f287d5df Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Fri, 3 Jul 2026 01:42:34 -0400 Subject: [PATCH 04/26] Also remove fieldset for npc features and fix padding issue in full screen notes --- .../sheets/actors/actor-sheet-shared.less | 3 ++- styles/less/sheets/actors/npc/features.less | 8 ++----- templates/sheets/actors/npc/features.hbs | 21 ++++++++++--------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/styles/less/sheets/actors/actor-sheet-shared.less b/styles/less/sheets/actors/actor-sheet-shared.less index 37e8579f..3e233013 100644 --- a/styles/less/sheets/actors/actor-sheet-shared.less +++ b/styles/less/sheets/actors/actor-sheet-shared.less @@ -79,6 +79,7 @@ .editor-content { scrollbar-gutter: stable; padding-right: @right-padding; + padding-bottom: 4px; } &.inactive { button.toggle { @@ -89,7 +90,7 @@ } } &.active { - padding: 8px 0 4px 16px; + padding: 8px 0 0 16px; } } diff --git a/styles/less/sheets/actors/npc/features.less b/styles/less/sheets/actors/npc/features.less index a579d9f8..f68df8a8 100644 --- a/styles/less/sheets/actors/npc/features.less +++ b/styles/less/sheets/actors/npc/features.less @@ -7,12 +7,8 @@ } .feature-section { - display: flex; - flex-direction: column; - gap: 10px; - overflow-y: auto; - padding-bottom: 4px; - .with-scroll-shadows(); + padding: 16px calc(16px - var(--scrollbar-width)) 4px 16px; + .stable-scroll-container(); } } } diff --git a/templates/sheets/actors/npc/features.hbs b/templates/sheets/actors/npc/features.hbs index 3b495e74..05e95737 100644 --- a/templates/sheets/actors/npc/features.hbs +++ b/templates/sheets/actors/npc/features.hbs @@ -1,14 +1,15 @@
-
- {{> 'daggerheart.inventory-items' - title=tabs.features.label - type='feature' - collection=@root.features - hideContextMenu=true - hideModifyControls=true - canCreate=@root.editable - showActions=@root.editable - }} +
+ {{#each @root.features as |item|}} + {{> "daggerheart.inventory-item" + item=item + type="feature" + actorType=@root.document.type + hideContextMenu=true + hideModifyControls=true + showActions=@root.editable + }} + {{/each}}
\ No newline at end of file From 3faf588e6c361555d6985b2e94574b3ea89db557 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Sun, 12 Jul 2026 20:43:56 -0400 Subject: [PATCH 05/26] Fix chat messages with list items or weapon/armor features (#2081) --- module/data/item/base.mjs | 2 +- module/documents/item.mjs | 5 +--- styles/less/global/inventory-item.less | 32 +---------------------- styles/less/global/prose-mirror.less | 31 +--------------------- styles/less/ui/chat/ability-use.less | 4 ++- styles/less/utils/mixin.less | 36 +++++++++++++++++++++++++- 6 files changed, 42 insertions(+), 68 deletions(-) diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index ba114fda..131ef10f 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -143,7 +143,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { /** * Gets the enriched and augmented description for the item. * @param {object} [options] - Options that modify the styling of the rendered template. { headerStyle: undefined|'none'|'large' } - * @returns {string} + * @returns {Promise} */ async getEnrichedDescription() { if (!this.metadata.hasDescription) return ''; diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 14717538..8112e99f 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -208,10 +208,7 @@ export default class DHItem extends foundry.documents.Item { tags: this._getTags() }, actions: item.system.actionsList, - description: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.system.description, { - relativeTo: this.parent, - rollData: this.parent?.getRollData() ?? {} - }) + description: await this.system.getEnrichedDescription() }; const msg = { diff --git a/styles/less/global/inventory-item.less b/styles/less/global/inventory-item.less index 2e6cc863..d942133c 100644 --- a/styles/less/global/inventory-item.less +++ b/styles/less/global/inventory-item.less @@ -163,37 +163,7 @@ } .inventory-description { overflow: hidden; - - h1 { - font-size: var(--font-size-32); - } - h2 { - font-size: var(--font-size-28); - font-weight: 600; - } - h3 { - font-size: var(--font-size-20); - font-weight: 600; - } - h4 { - font-size: var(--font-size-16); - color: @beige; - font-weight: 600; - } - - ul, - ol { - margin: 1rem 0; - padding: 0 0 0 1.25rem; - - li { - margin-bottom: 0.25rem; - } - } - - ul { - list-style: disc; - } + .typography(); } } .item-resources { diff --git a/styles/less/global/prose-mirror.less b/styles/less/global/prose-mirror.less index 27048ddf..430ca79d 100644 --- a/styles/less/global/prose-mirror.less +++ b/styles/less/global/prose-mirror.less @@ -14,36 +14,7 @@ } .editor-content { .with-scroll-shadows(); - h1 { - font-size: var(--font-size-32); - } - h2 { - font-size: var(--font-size-28); - font-weight: 600; - } - h3 { - font-size: var(--font-size-20); - font-weight: 600; - } - h4 { - font-size: var(--font-size-16); - color: light-dark(@dark, @beige); - font-weight: 600; - } - - ul, - ol { - margin: 1rem 0; - padding: 0 0 0 1.25rem; - - li { - margin-bottom: 0.25rem; - } - } - - ul { - list-style: disc; - } + .typography(); } // Fixes centering and makes it not render over scrollbar &:hover button.toggle:enabled { diff --git a/styles/less/ui/chat/ability-use.less b/styles/less/ui/chat/ability-use.less index d028fa7a..c31136ad 100644 --- a/styles/less/ui/chat/ability-use.less +++ b/styles/less/ui/chat/ability-use.less @@ -117,7 +117,9 @@ } .description { - padding: 8px; + padding: 0; + margin: 8px; + .typography(); } .ability-card-footer { diff --git a/styles/less/utils/mixin.less b/styles/less/utils/mixin.less index 2ce85166..fb70d0a3 100644 --- a/styles/less/utils/mixin.less +++ b/styles/less/utils/mixin.less @@ -203,4 +203,38 @@ overflow-y: auto; scrollbar-gutter: stable; .with-scroll-shadows(); -} \ No newline at end of file +} + +/** Typography stylings for most longform text, usually item descriptions */ +.typography() { + h1 { + font-size: var(--font-size-32); + } + h2 { + font-size: var(--font-size-28); + font-weight: 600; + } + h3 { + font-size: var(--font-size-20); + font-weight: 600; + } + h4 { + font-size: var(--font-size-16); + color: light-dark(@dark, @beige); + font-weight: 600; + } + + ul, + ol { + margin: 1rem 0; + padding: 0 0 0 1.25rem; + + li { + margin-bottom: 0.25rem; + } + } + + ul { + list-style: disc; + } +} From 3de9c2f9095620d0f6f32445e77934eb90cb633c Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Mon, 13 Jul 2026 08:56:49 -0400 Subject: [PATCH 06/26] Remove duplicate action descriptions in environments (#2083) --- ...ment_Abandoned_Grove_pGEdzdLkqYtBhxnG.json | 2 +- ...nvironment_Ambushers_uXZpebPR77YQ1oXI.json | 10 ++--- ...ronment_Castle_Siege_1eZ32Esq7rfZOjlu.json | 2 +- ...ent_Cliffside_Ascent_LPpfdlNKqiZIl04w.json | 12 +++--- ...ironment_Cult_Ritual_QAXXiOKBDmCTauHD.json | 38 +++++++++---------- ...ment_Hallowed_Temple_dsA6j69AnaJhUyqH.json | 12 +++--- ...ronment_Haunted_City_OzYbizKraK92FDiI.json | 10 ++--- ...nment_Imperial_Court_jr1xAoXzVwVblzxI.json | 10 ++--- ...ecromancer_s_Ossuary_h3KyRL7AshhLAmcH.json | 6 +-- ...nment_Pitched_Battle_EWD3ZsLoK6VMVOf7.json | 2 +- 10 files changed, 46 insertions(+), 58 deletions(-) diff --git a/src/packs/environments/environment_Abandoned_Grove_pGEdzdLkqYtBhxnG.json b/src/packs/environments/environment_Abandoned_Grove_pGEdzdLkqYtBhxnG.json index 23c1d966..039eafcf 100644 --- a/src/packs/environments/environment_Abandoned_Grove_pGEdzdLkqYtBhxnG.json +++ b/src/packs/environments/environment_Abandoned_Grove_pGEdzdLkqYtBhxnG.json @@ -391,7 +391,7 @@ "type": "effect", "_id": "p6V4k4yMwJ1UPZMz", "systemPath": "actions", - "description": "

Spend a Fear to summon a @UUID[Compendium.daggerheart.adversaries.Actor.sRn4bqerfARvhgSV]{Minor Chaos Elemental} drawn to the echoes of violence and discord. They appear within Far range of a chosen PC and immediately take the spotlight.

What color does the grass turn as the elemental appears? How does the chaos warp insects and small wildlife within the grove?

", + "description": "", "chatDisplay": true, "actionType": "action", "cost": [], diff --git a/src/packs/environments/environment_Ambushers_uXZpebPR77YQ1oXI.json b/src/packs/environments/environment_Ambushers_uXZpebPR77YQ1oXI.json index e8ba889a..582e7ec0 100644 --- a/src/packs/environments/environment_Ambushers_uXZpebPR77YQ1oXI.json +++ b/src/packs/environments/environment_Ambushers_uXZpebPR77YQ1oXI.json @@ -35,12 +35,9 @@ "src": "systems/daggerheart/assets/icons/documents/actors/forest.svg", "anchorX": 0.5, "anchorY": 0.5, - "offsetX": 0, - "offsetY": 0, "fit": "contain", "scaleX": 1, "scaleY": 1, - "rotation": 0, "tint": "#ffffff", "alphaThreshold": 0.75 }, @@ -91,7 +88,7 @@ "saturation": 0, "contrast": 0 }, - "detectionModes": [], + "detectionModes": {}, "occludable": { "radius": 0 }, @@ -117,7 +114,8 @@ "flags": {}, "randomImg": false, "appendNumber": false, - "prependAdjective": false + "prependAdjective": false, + "depth": 1 }, "items": [ { @@ -156,7 +154,7 @@ "type": "effect", "_id": "6DKa1Pm605HpChPd", "systemPath": "actions", - "description": "

When a PC starts the ambush on unsuspecting adversaries, you lose 2 Fear and the first attack roll a PC makes has advantage.

What are the adversaries in the middle of doing when the ambush starts? How does this impact their approach to the fight?

", + "description": "", "chatDisplay": true, "actionType": "action", "cost": [ diff --git a/src/packs/environments/environment_Castle_Siege_1eZ32Esq7rfZOjlu.json b/src/packs/environments/environment_Castle_Siege_1eZ32Esq7rfZOjlu.json index 190d78b1..0df61deb 100644 --- a/src/packs/environments/environment_Castle_Siege_1eZ32Esq7rfZOjlu.json +++ b/src/packs/environments/environment_Castle_Siege_1eZ32Esq7rfZOjlu.json @@ -330,7 +330,7 @@ "type": "attack", "_id": "r5JN5oFYL5DC6Qqw", "systemPath": "actions", - "description": "

When an adversary is defeated, you can spend a Fear to have a stray attack from a siege weapon hit a point on the battlefield. All targets within Very Close range of that point must make an Agility Reaction Roll.

  • Targets who fail take 3d8+3 physical or magic damage and must mark a Stress.

  • Targets who succeed must mark a Stress.

What debris is scattered by the attack? What is broken by the strike that can’t be easily mended?

", + "description": "", "chatDisplay": true, "actionType": "action", "cost": [], diff --git a/src/packs/environments/environment_Cliffside_Ascent_LPpfdlNKqiZIl04w.json b/src/packs/environments/environment_Cliffside_Ascent_LPpfdlNKqiZIl04w.json index ef367d67..c65ede29 100644 --- a/src/packs/environments/environment_Cliffside_Ascent_LPpfdlNKqiZIl04w.json +++ b/src/packs/environments/environment_Cliffside_Ascent_LPpfdlNKqiZIl04w.json @@ -45,12 +45,9 @@ "src": "systems/daggerheart/assets/icons/documents/actors/forest.svg", "anchorX": 0.5, "anchorY": 0.5, - "offsetX": 0, - "offsetY": 0, "fit": "contain", "scaleX": 1, "scaleY": 1, - "rotation": 0, "tint": "#ffffff", "alphaThreshold": 0.75 }, @@ -101,7 +98,7 @@ "saturation": 0, "contrast": 0 }, - "detectionModes": [], + "detectionModes": {}, "occludable": { "radius": 0 }, @@ -127,7 +124,8 @@ "flags": {}, "randomImg": false, "appendNumber": false, - "prependAdjective": false + "prependAdjective": false, + "depth": 1 }, "items": [ { @@ -204,7 +202,7 @@ "type": "damage", "_id": "p1UiGEiGyl6r7PrA", "systemPath": "actions", - "description": "

Previous climbers left behind large metal rods that climbers can use to aid their ascent. If a PC using the pitons fails an action roll to climb, they can mark a Stress instead of ticking the countdown up.

What do the shape and material of these pitons tell you about the previous climbers? How far apart are they from one another?

", + "description": "", "chatDisplay": true, "actionType": "action", "cost": [], @@ -435,7 +433,7 @@ "type": "effect", "_id": "M8MfD2qBfYCwNKvH", "systemPath": "actions", - "description": "

Spend a Fear to have a PC’s handhold fail, plummeting them toward the ground. If they aren’t saved on the next action, they hit the ground and tick up the countdown by 2. The PC takes 1d12 physical damage if the countdown is between 8 and 12, 2d12 between 4 and 7, and 3d12 at 3 or lower.

How can you tell many others have fallen here before? What lives in these walls that might try to scare adventurers into falling for an easy meal?

", + "description": "", "chatDisplay": true, "actionType": "action", "cost": [ diff --git a/src/packs/environments/environment_Cult_Ritual_QAXXiOKBDmCTauHD.json b/src/packs/environments/environment_Cult_Ritual_QAXXiOKBDmCTauHD.json index d6809cd1..e3e90c26 100644 --- a/src/packs/environments/environment_Cult_Ritual_QAXXiOKBDmCTauHD.json +++ b/src/packs/environments/environment_Cult_Ritual_QAXXiOKBDmCTauHD.json @@ -45,12 +45,9 @@ "src": "systems/daggerheart/assets/icons/documents/actors/forest.svg", "anchorX": 0.5, "anchorY": 0.5, - "offsetX": 0, - "offsetY": 0, "fit": "contain", "scaleX": 1, "scaleY": 1, - "rotation": 0, "tint": "#ffffff", "alphaThreshold": 0.75 }, @@ -101,7 +98,7 @@ "saturation": 0, "contrast": 0 }, - "detectionModes": [], + "detectionModes": {}, "occludable": { "radius": 0 }, @@ -127,7 +124,8 @@ "flags": {}, "randomImg": false, "appendNumber": false, - "prependAdjective": false + "prependAdjective": false, + "depth": 1 }, "items": [ { @@ -236,7 +234,7 @@ "type": "effect", "_id": "EATw4ZkcuGeDfgLZ", "systemPath": "actions", - "description": "

A portion of the ritual’s power is diverted into a cult member to fight off interlopers. Choose one adversary to become Imbued with terrible magic until the scene ends or they’re defeated. An Imbued adversary immediately takes the spotlight and gains one of the following benefits, or all three if you spend a Fear:

  • They gain advantage on all attacks.

  • They deal an extra 1d10 damage on a successful attack.

  • They gain the following feature: Relentless (2) - Passive. This adversary can be spotlighted up to two times per GM turn. Spend Fear as usual to spotlight them.

How does the enemy change in appearance? What fears do their blows bring to the surface?

", + "description": "", "chatDisplay": true, "actionType": "action", "cost": [], @@ -283,13 +281,10 @@ }, "disabled": false, "duration": { - "startTime": null, - "combat": null, - "seconds": null, - "rounds": null, - "turns": null, - "startRound": null, - "startTurn": null + "value": null, + "units": "seconds", + "expiry": null, + "expired": false }, "description": "

An Imbued adversary immediately takes the spotlight and gains one of the following benefits, or all three if you spend a Fear:

  • They gain advantage on all attacks.

  • They deal an extra 1d10 damage on a successful attack.

  • They gain the following feature: Relentless (2) - Passive. This adversary can be spotlighted up to two times per GM turn. Spend Fear as usual to spotlight them.

How does the enemy change in appearance? What fears do their blows bring to the surface?

", "tint": "#ffffff", @@ -299,6 +294,9 @@ "_stats": { "compendiumSource": null }, + "start": null, + "showIcon": 1, + "folder": null, "_key": "!actors.items.effects!QAXXiOKBDmCTauHD.0Rgqw1kUPeJ11ldd.dYQBQq1xIysM0qLo" }, { @@ -326,13 +324,10 @@ }, "disabled": true, "duration": { - "startTime": null, - "combat": null, - "seconds": null, - "rounds": null, - "turns": null, - "startRound": null, - "startTurn": null + "value": null, + "units": "seconds", + "expiry": null, + "expired": false }, "description": "", "tint": "#ffffff", @@ -342,6 +337,9 @@ "_stats": { "compendiumSource": null }, + "start": null, + "showIcon": 1, + "folder": null, "_key": "!actors.items.effects!QAXXiOKBDmCTauHD.0Rgqw1kUPeJ11ldd.Hxw5lXE77bGzuaOu" } ], diff --git a/src/packs/environments/environment_Hallowed_Temple_dsA6j69AnaJhUyqH.json b/src/packs/environments/environment_Hallowed_Temple_dsA6j69AnaJhUyqH.json index c510a87f..d8b04d22 100644 --- a/src/packs/environments/environment_Hallowed_Temple_dsA6j69AnaJhUyqH.json +++ b/src/packs/environments/environment_Hallowed_Temple_dsA6j69AnaJhUyqH.json @@ -44,12 +44,9 @@ "src": "systems/daggerheart/assets/icons/documents/actors/forest.svg", "anchorX": 0.5, "anchorY": 0.5, - "offsetX": 0, - "offsetY": 0, "fit": "contain", "scaleX": 1, "scaleY": 1, - "rotation": 0, "tint": "#ffffff", "alphaThreshold": 0.75 }, @@ -100,7 +97,7 @@ "saturation": 0, "contrast": 0 }, - "detectionModes": [], + "detectionModes": {}, "occludable": { "radius": 0 }, @@ -126,7 +123,8 @@ "flags": {}, "randomImg": false, "appendNumber": false, - "prependAdjective": false + "prependAdjective": false, + "depth": 1 }, "items": [ { @@ -140,7 +138,7 @@ "type": "healing", "_id": "uLCoTKa7Jn2HaRqR", "systemPath": "actions", - "description": "

A PC who takes a rest in the Hallowed Temple automatically clears all HP.

What does the incense smell like? What kinds of songs do the acolytes sing?

", + "description": "", "chatDisplay": true, "actionType": "action", "cost": [], @@ -337,7 +335,7 @@ "type": "effect", "_id": "pJVipg7CbA9CB0Um", "systemPath": "actions", - "description": "

When the PCs have trespassed, blasphemed, or offended the clergy, you can spend a Fear to summon a @UUID[Compendium.daggerheart.adversaries.Actor.r1mbfSSwKWdcFdAU]{High Seraph} and [[/r 1d4]] @UUID[Compendium.daggerheart.adversaries.Actor.B4LZcGuBAHzyVdzy]{Bladed Guard} within Close range of the senior priest to reinforce their will.

What symbols or icons do they bear that signal they are anointed agents of the divinity? Who leads the group and what led them to this calling?

", + "description": "", "chatDisplay": true, "actionType": "action", "cost": [ diff --git a/src/packs/environments/environment_Haunted_City_OzYbizKraK92FDiI.json b/src/packs/environments/environment_Haunted_City_OzYbizKraK92FDiI.json index 564612cb..bbc3ef0a 100644 --- a/src/packs/environments/environment_Haunted_City_OzYbizKraK92FDiI.json +++ b/src/packs/environments/environment_Haunted_City_OzYbizKraK92FDiI.json @@ -44,12 +44,9 @@ "src": "systems/daggerheart/assets/icons/documents/actors/forest.svg", "anchorX": 0.5, "anchorY": 0.5, - "offsetX": 0, - "offsetY": 0, "fit": "contain", "scaleX": 1, "scaleY": 1, - "rotation": 0, "tint": "#ffffff", "alphaThreshold": 0.75 }, @@ -100,7 +97,7 @@ "saturation": 0, "contrast": 0 }, - "detectionModes": [], + "detectionModes": {}, "occludable": { "radius": 0 }, @@ -126,7 +123,8 @@ "flags": {}, "randomImg": false, "appendNumber": false, - "prependAdjective": false + "prependAdjective": false, + "depth": 1 }, "items": [ { @@ -282,7 +280,7 @@ "type": "countdown", "_id": "VhqZKDA4032i8zY3", "systemPath": "actions", - "description": "

Spend a Fear to manifest the echo of a past disaster that ravaged the city. Activate a Progress Countdown (5) as the disaster replays around the PCs. To complete the countdown and escape the catastrophe, the PCs must overcome threats such as rampaging fires, stampeding civilians, collapsing buildings, or crumbling streets, while recalling history and finding clues to escape the inevitable.

Is this the disaster that led the city to be abandoned? What is known about this disaster and how could that help the PCs escape?

", + "description": "", "chatDisplay": true, "originItem": { "type": "itemCollection" diff --git a/src/packs/environments/environment_Imperial_Court_jr1xAoXzVwVblzxI.json b/src/packs/environments/environment_Imperial_Court_jr1xAoXzVwVblzxI.json index 5807d43c..93851e3c 100644 --- a/src/packs/environments/environment_Imperial_Court_jr1xAoXzVwVblzxI.json +++ b/src/packs/environments/environment_Imperial_Court_jr1xAoXzVwVblzxI.json @@ -46,12 +46,9 @@ "src": "systems/daggerheart/assets/icons/documents/actors/forest.svg", "anchorX": 0.5, "anchorY": 0.5, - "offsetX": 0, - "offsetY": 0, "fit": "contain", "scaleX": 1, "scaleY": 1, - "rotation": 0, "tint": "#ffffff", "alphaThreshold": 0.75 }, @@ -102,7 +99,7 @@ "saturation": 0, "contrast": 0 }, - "detectionModes": [], + "detectionModes": {}, "occludable": { "radius": 0 }, @@ -128,7 +125,8 @@ "flags": {}, "randomImg": false, "appendNumber": false, - "prependAdjective": false + "prependAdjective": false, + "depth": 1 }, "items": [ { @@ -297,7 +295,7 @@ "type": "attack", "_id": "9ipckCFMz9DVw8ab", "systemPath": "actions", - "description": "

Spend a Fear to tick down a long-term countdown related to the empire’s agenda by [[/r 1d4]]. If this triggers the countdown, a proclamation related to the agenda is announced at court as the plan is executed.

What display of power or transfer of wealth was needed to expedite this plan? Whose lives were disrupted or upended to make this happen?

", + "description": "", "chatDisplay": true, "actionType": "action", "cost": [], diff --git a/src/packs/environments/environment_Necromancer_s_Ossuary_h3KyRL7AshhLAmcH.json b/src/packs/environments/environment_Necromancer_s_Ossuary_h3KyRL7AshhLAmcH.json index 299e8729..3d87930c 100644 --- a/src/packs/environments/environment_Necromancer_s_Ossuary_h3KyRL7AshhLAmcH.json +++ b/src/packs/environments/environment_Necromancer_s_Ossuary_h3KyRL7AshhLAmcH.json @@ -137,7 +137,7 @@ "type": "damage", "_id": "jVY198vniaTSlgsX", "systemPath": "actions", - "description": "

A feature or action that clears HP requires spending a Hope to use. If it already costs Hope, a PC must spend an additional Hope.

What does it feel like to try to heal in a place so antithetical to life?

", + "description": "", "chatDisplay": true, "actionType": "action", "cost": [], @@ -240,7 +240,7 @@ "type": "attack", "_id": "M1mOwi4Limw2hRwL", "systemPath": "actions", - "description": "

All targets within Close range of a point you choose in this environment must succeed on an Agility Reaction Roll or take 4d8+8 physical damage from skeletal shrapnel as part of the ossuary detonates around them.

What ancient skeletal architecture is destroyed? What bones stick in your armor?

", + "description": "", "chatDisplay": true, "actionType": "action", "cost": [], @@ -421,7 +421,7 @@ "type": "effect", "_id": "hFeTdiHWeCYkb8Hg", "systemPath": "actions", - "description": "

Spend a Fear to summon [[/r 1d6]] @UUID[Compendium.daggerheart.adversaries.Actor.gP3fWTLzSFnpA8EJ]{Rotted Zombie}, two @UUID[Compendium.daggerheart.adversaries.Actor.CP6iRfHdyFWniTHY]{Perfected Zombie}, or a @UUID[Compendium.daggerheart.adversaries.Actor.YhJrP7rTBiRdX5Fp]{Zombie Legion}, who appear at Close range of a chosen PC.

Who were these people before they became the necromancer’s pawns? What vestiges of those lives remain for the heroes to see?

", + "description": "", "chatDisplay": true, "actionType": "action", "cost": [ diff --git a/src/packs/environments/environment_Pitched_Battle_EWD3ZsLoK6VMVOf7.json b/src/packs/environments/environment_Pitched_Battle_EWD3ZsLoK6VMVOf7.json index 42fbd8f9..120b92a0 100644 --- a/src/packs/environments/environment_Pitched_Battle_EWD3ZsLoK6VMVOf7.json +++ b/src/packs/environments/environment_Pitched_Battle_EWD3ZsLoK6VMVOf7.json @@ -199,7 +199,7 @@ "type": "attack", "_id": "1giAFbu3tGqXwi8g", "systemPath": "actions", - "description": "

Spend a Fear as a mage from one side uses large-scale destructive magic. Pick a point on the battlefield within Very Far range of the mage. All targets within Close range of that point must make an Agility Reaction Roll. Targets who fail take 3d12+8 magic damage and must mark a Stress.

What form does the attack take—fireball raining acid a storm of blades? What tactical objective is this attack meant to accomplish and what comes next?

", + "description": "", "chatDisplay": true, "actionType": "action", "cost": [ From 6e0d0b4e2cfb1fd7b4ff8bb15b1fe2fd73b139d0 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Mon, 13 Jul 2026 08:57:55 -0400 Subject: [PATCH 07/26] Adjust styling of secret blocks (#2080) * Adjust styling of secret blocks * Only show button when hovering over the secret section --- styles/less/global/elements.less | 38 ++++++++++++++++++++++++++++++++ styles/less/global/sheet.less | 4 ++-- styles/less/utils/colors.less | 4 ++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index e35f527a..edc02f9a 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -597,6 +597,44 @@ font-size: var(--font-size-12); padding-left: 3px; } + + secret-block { + position: relative; + section.secret { + background-color: @red-10; + padding: 0; + margin-top: 0.375rem; + &.revealed { + background-color: @green-10; + } + p { + margin: 0.5rem 0; + } + } + button.reveal { + --button-size: 0.875rem; + position: absolute; + margin: auto; + left: 0; + right: 0; + width: min-content; + padding: 1px 8px 0 8px; + bottom: calc(100% - 0.4375rem - 2px); + + background-color: var(--dh-window-button-color-bg); // todo: find a better var name + border-color: var(--color-secret-border); + color: var(--dh-window-button-color-text); + font-size: var(--font-size-10); + user-select: none; + text-transform: uppercase; + + visibility: hidden; + } + + &:hover button.reveal { + visibility: visible; + } + } } .system-daggerheart { diff --git a/styles/less/global/sheet.less b/styles/less/global/sheet.less index 8381c7c3..d7be1a84 100755 --- a/styles/less/global/sheet.less +++ b/styles/less/global/sheet.less @@ -36,8 +36,8 @@ body.game:is(.performance-low, .noblur) { } button { - background: light-dark(#e8e6e3, @deep-black); - color: light-dark(@dark-blue, @beige); + background: var(--dh-window-button-color-bg); + color: var(--dh-window-button-color-text); border: 1px solid light-dark(@dark-blue, transparent); padding: 0; diff --git a/styles/less/utils/colors.less b/styles/less/utils/colors.less index bb219ebb..d9358112 100755 --- a/styles/less/utils/colors.less +++ b/styles/less/utils/colors.less @@ -107,6 +107,8 @@ --dh-input-color-text: @dark; --dh-trait-color-bg: #b1afb6; --dh-trait-color-border: #8e8d96; + --dh-window-button-color-bg: #e8e6e3; + --dh-window-button-color-text: @dark-blue; } } @scope (.theme-dark) to (.themed) { @@ -124,6 +126,8 @@ --dh-input-color-text: @beige; --dh-trait-color-bg: #50433F; --dh-trait-color-border: #927952; + --dh-window-button-color-bg: @deep-black; + --dh-window-button-color-text: @beige; } } From 7b35feb36dd71b548dbb004a3c149044cc47e7a5 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Mon, 13 Jul 2026 15:01:03 +0200 Subject: [PATCH 08/26] Corrected translation for damageReductionOnlyMagical (#2084) --- lang/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/en.json b/lang/en.json index 7863fb67..99b6baeb 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2245,7 +2245,7 @@ "hint": "A used armor slot normally reduces damage by one step. This value increases the number of steps damage is reduced by." }, "magical": { - "label": "Daamge Reduction: Only Magical", + "label": "Damage Reduction: Only Magical", "hint": "Armor can only be used to reduce magical damage" }, "maxArmorMarkedBonus": "Max Armor Used", From d3d9ddfb4188a55fa5d13ccb0d90e8c8f1e5c12d Mon Sep 17 00:00:00 2001 From: WBHarry Date: Mon, 13 Jul 2026 17:02:26 +0200 Subject: [PATCH 09/26] Fixed an issue with the 2.5.2 migration --- module/systemRegistration/migration-handlers/2_5_2.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/systemRegistration/migration-handlers/2_5_2.mjs b/module/systemRegistration/migration-handlers/2_5_2.mjs index f6d9ea77..944f0eec 100644 --- a/module/systemRegistration/migration-handlers/2_5_2.mjs +++ b/module/systemRegistration/migration-handlers/2_5_2.mjs @@ -15,7 +15,7 @@ export class Migration_2_5_2 extends MigrationHandlerBase { const srdEffect = srdItem?.effects.find(x => x.name === effectSource.name); if (change.type === 'custom') { const srdChange = srdEffect ? srdEffect.system.changes[i] : null; - if ( + if (srdChange && change.key === srdChange.key && change.value === srdChange.value && change.type !== srdChange.type From 81e264a4779fc86e7c7b2593b7d8c5e76d30d381 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Mon, 13 Jul 2026 17:04:27 +0200 Subject: [PATCH 10/26] Raised version --- system.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system.json b/system.json index 43e06254..37242137 100644 --- a/system.json +++ b/system.json @@ -2,7 +2,7 @@ "id": "daggerheart", "title": "Daggerheart", "description": "An unofficial implementation of the Daggerheart system", - "version": "2.5.2", + "version": "2.5.3", "compatibility": { "minimum": "14.364", "verified": "14.364", @@ -10,7 +10,7 @@ }, "url": "https://github.com/Foundryborne/daggerheart", "manifest": "https://raw.githubusercontent.com/Foundryborne/daggerheart/v14/system.json", - "download": "https://github.com/Foundryborne/daggerheart/releases/download/2.5.2/system.zip", + "download": "https://github.com/Foundryborne/daggerheart/releases/download/2.5.3/system.zip", "authors": [ { "name": "WBHarry" From 450287e4d05d0f3dd3f3d5f3e29558d8462e4c9c Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Mon, 13 Jul 2026 22:57:43 +0200 Subject: [PATCH 11/26] [Fix] Summon Wildcard Handling (#2086) --- module/data/action/baseAction.mjs | 3 ++- module/data/fields/action/summonField.mjs | 18 +++++++++-------- module/data/fields/actionField.mjs | 4 ++-- module/documents/tokenManager.mjs | 24 +++++++++++++---------- templates/ui/chat/action.hbs | 6 +++--- 5 files changed, 31 insertions(+), 24 deletions(-) diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index be7224cd..5b871c9a 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -256,7 +256,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return; - if (this.chatDisplay && !config.skips.createMessage && !config.actionChatMessageHandled) await this.toChat(); + if (this.chatDisplay && !config.skips.createMessage && !config.actionChatMessageHandled) + await this.toChat(null, config); return config; } diff --git a/module/data/fields/action/summonField.mjs b/module/data/fields/action/summonField.mjs index 6845d2ba..fef6625a 100644 --- a/module/data/fields/action/summonField.mjs +++ b/module/data/fields/action/summonField.mjs @@ -23,7 +23,7 @@ export default class DHSummonField extends fields.ArrayField { super(summonFields, options, context); } - static async execute() { + static async execute(config) { if (!canvas.scene) { ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.summon.error')); return; @@ -36,6 +36,7 @@ export default class DHSummonField extends fields.ArrayField { const rolls = []; const summonData = []; + const chatMessageData = []; for (const summon of this.summon) { const roll = new Roll(itemAbleRollParse(summon.count, this.actor, this.item)); await roll.evaluate(); @@ -54,17 +55,18 @@ export default class DHSummonField extends fields.ArrayField { tokenPreviewName: `${actor.prototypeToken.name}${remaining > 1 ? ` (${remaining}x)` : ''}` }); } + + chatMessageData.push({ + data: actor, + quantity: countNumber + }); } if (rolls.length) await triggerChatRollFx(rolls); this.actor.sheet?.minimize(); - DHSummonField.handleSummon(summonData, this.actor); - } - - static async handleSummon(summonData, actionActor) { - await CONFIG.ux.TokenManager.createTokensWithPreview(summonData, { elevation: actionActor.token?.elevation }); - - return actionActor.sheet?.maximize(); + await CONFIG.ux.TokenManager.createTokensWithPreview(summonData, { elevation: this.actor.token?.elevation }); + this.actor.sheet?.maximize(); + config.summonData = chatMessageData; } } diff --git a/module/data/fields/actionField.mjs b/module/data/fields/actionField.mjs index 83672c8e..af8f338e 100644 --- a/module/data/fields/actionField.mjs +++ b/module/data/fields/actionField.mjs @@ -269,7 +269,7 @@ export function ActionMixin(Base) { return this.delete(); } - async toChat(origin) { + async toChat(origin, config) { const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance) .expandRollMessage?.desc; @@ -282,7 +282,7 @@ export function ActionMixin(Base) { img: this.baseAction ? this.parent.parent.img : this.img, tags: this.tags ? this.tags : ['Spell', 'Arcana', 'Lv 10'], areas: this.areas, - summon: this.summon + summon: config?.summonData }, source: { actor: this.actor.uuid, diff --git a/module/documents/tokenManager.mjs b/module/documents/tokenManager.mjs index 7678d2c7..062a22ee 100644 --- a/module/documents/tokenManager.mjs +++ b/module/documents/tokenManager.mjs @@ -19,10 +19,10 @@ export default class DhTokenManager { } } - return await canvas.tokens.placeTokens( + const placedData = await canvas.tokens.placeTokens( [ { - ...actor.prototypeToken.toObject(), + ...(await actor.getTokenDocument()).toObject(), actorId: actor.id, displayName: 50, ...tokenData @@ -30,6 +30,8 @@ export default class DhTokenManager { ], { create: false } ); + + return placedData[0] ?? null; } /** @@ -46,22 +48,24 @@ export default class DhTokenManager { const createElevation = elevation ?? level.elevation.bottom; for (const tokenData of tokensData) { - const previewTokens = await this.createPreview(tokenData.actor, { + const previewToken = await this.createPreview(tokenData.actor, { name: tokenData.tokenPreviewName, level: game.user.viewedLevel, elevation: createElevation, flags: { daggerheart: { createPlacement: true } } }); - if (!previewTokens?.length) return null; + if (!previewToken) return null; + + const finalTokenData = { + ...previewToken.toObject(), + name: tokenData.actor.prototypeToken.name, + displayName: tokenData.actor.prototypeToken.displayName, + flags: tokenData.actor.prototypeToken.flags + }; await canvas.scene.createEmbeddedDocuments( 'Token', - previewTokens.map(x => ({ - ...x.toObject(), - name: tokenData.actor.prototypeToken.name, - displayName: tokenData.actor.prototypeToken.displayName, - flags: tokenData.actor.prototypeToken.flags - })), + [finalTokenData], { controlObject: true, parent: canvas.scene } ); } diff --git a/templates/ui/chat/action.hbs b/templates/ui/chat/action.hbs index 51840363..d9ceb417 100644 --- a/templates/ui/chat/action.hbs +++ b/templates/ui/chat/action.hbs @@ -16,10 +16,10 @@ {{#each action.summon}}
- - + +
- # {{this.rolledCount}} + # {{this.quantity}}
{{/each}} From 02a73d774a916f20b6816590fbc8680dcd60b032 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Mon, 13 Jul 2026 18:31:42 -0400 Subject: [PATCH 12/26] Add guard for null placedData (#2087) --- module/documents/tokenManager.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/documents/tokenManager.mjs b/module/documents/tokenManager.mjs index 062a22ee..b21b56a0 100644 --- a/module/documents/tokenManager.mjs +++ b/module/documents/tokenManager.mjs @@ -31,7 +31,7 @@ export default class DhTokenManager { { create: false } ); - return placedData[0] ?? null; + return placedData?.[0] ?? null; } /** From 3a5529f1dc8730c217f5fa3cf033974775de556f Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Mon, 13 Jul 2026 18:56:34 -0400 Subject: [PATCH 13/26] Cleanup secret block styling (#2088) --- styles/less/global/elements.less | 39 ++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index edc02f9a..1b7ed072 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -599,27 +599,23 @@ } secret-block { - position: relative; - section.secret { - background-color: @red-10; - padding: 0; - margin-top: 0.375rem; - &.revealed { - background-color: @green-10; - } - p { - margin: 0.5rem 0; - } - } + display: block; + + /** A buffer to make the hover behavior work a bit better. The bottom in the button needs to compensate */ + @buffer: 8px; + margin-top: -@buffer; + padding-top: @buffer; + button.reveal { - --button-size: 0.875rem; + --button-size: 1rem; + height: var(--button-size); position: absolute; margin: auto; left: 0; right: 0; width: min-content; padding: 1px 8px 0 8px; - bottom: calc(100% - 0.4375rem - 2px); + bottom: calc(100% - 0.4375rem - 1px); background-color: var(--dh-window-button-color-bg); // todo: find a better var name border-color: var(--color-secret-border); @@ -627,6 +623,7 @@ font-size: var(--font-size-10); user-select: none; text-transform: uppercase; + white-space: nowrap; visibility: hidden; } @@ -635,6 +632,20 @@ visibility: visible; } } + + /** + * The element inside a secret-block. + * This is separate since during prosemirror editing, the secret-block container does not exist. + */ + section.secret { + --color-secret-bg: @red-10; + --color-revealed-bg: @green-10; + position: relative; + padding: 0; + p { + margin: 0.5rem 0; + } + } } .system-daggerheart { From 4974df16d071f67667df972d9fd27e0c4fee34e1 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Tue, 14 Jul 2026 08:35:02 -0400 Subject: [PATCH 14/26] Preserve description expand state on re-render (#2089) --- module/applications/dialogs/deathMove.mjs | 3 -- module/applications/dialogs/downtime.mjs | 6 +--- module/data/fields/actionField.mjs | 5 +-- module/documents/chatMessage.mjs | 42 ++++++++++++++--------- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/module/applications/dialogs/deathMove.mjs b/module/applications/dialogs/deathMove.mjs index 8e0ed6af..cfd7687b 100644 --- a/module/applications/dialogs/deathMove.mjs +++ b/module/applications/dialogs/deathMove.mjs @@ -185,8 +185,6 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV if (result === undefined) return; - const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance) - .expandRollMessage?.desc; const cls = getDocumentClass('ChatMessage'); const msg = { @@ -202,7 +200,6 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV img: this.selectedMove.img, description: game.i18n.localize(this.selectedMove.description), result: result, - open: autoExpandDescription ? 'open' : '', showRiskItAllButton: this.showRiskItAllButton, riskItAllButtonLabel: this.riskItAllButtonLabel, riskItAllHope: this.riskItAllHope diff --git a/module/applications/dialogs/downtime.mjs b/module/applications/dialogs/downtime.mjs index e209cc3b..5ba8e48e 100644 --- a/module/applications/dialogs/downtime.mjs +++ b/module/applications/dialogs/downtime.mjs @@ -196,9 +196,6 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV .filter(x => x.testUserPermission(game.user, 'LIMITED')) .filter(x => x.uuid !== this.actor.uuid); - const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance) - .expandRollMessage?.desc; - const cls = getDocumentClass('ChatMessage'); const msg = { user: game.user.id, @@ -219,8 +216,7 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV actor: { name: this.actor.name, img: this.actor.img }, moves: moves, characters: characters, - selfId: this.actor.uuid, - open: autoExpandDescription ? 'open' : '' + selfId: this.actor.uuid } ), flags: { diff --git a/module/data/fields/actionField.mjs b/module/data/fields/actionField.mjs index af8f338e..ba2fa37e 100644 --- a/module/data/fields/actionField.mjs +++ b/module/data/fields/actionField.mjs @@ -270,9 +270,6 @@ export function ActionMixin(Base) { } async toChat(origin, config) { - const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance) - .expandRollMessage?.desc; - const cls = getDocumentClass('ChatMessage'); const systemData = { title: game.i18n.localize('DAGGERHEART.CONFIG.FeatureForm.action'), @@ -307,7 +304,7 @@ export function ActionMixin(Base) { system: systemData, content: await foundry.applications.handlebars.renderTemplate( 'systems/daggerheart/templates/ui/chat/action.hbs', - { ...systemData, open: autoExpandDescription ? 'open' : '' } + systemData ), flags: { daggerheart: { diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index fd68997c..d53a76bd 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -3,6 +3,13 @@ import { emitGMUpdate, emitGMCreate, GMUpdateEvent } from '../systemRegistration export default class DhpChatMessage extends foundry.documents.ChatMessage { targetHook = null; + static #EXPAND_SECTIONS = [ + { selector: 'roll-section [data-action="expandRoll"]', key: 'roll' }, + { selector: 'damage-section', key: 'damage' }, + { selector: 'target-section', key: 'target' }, + { selector: 'description-section', key: 'desc' } + ]; + async renderHTML() { const actor = game.actors.get(this.speaker.actor); const actorData = @@ -89,23 +96,26 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { } } + // Check registered selectors and the main item section for expanding + // Preserving during re-render is handled by core foundry on anything with [data-action=expandRoll] const autoExpandRoll = game.settings.get( - CONFIG.DH.id, - CONFIG.DH.SETTINGS.gameSettings.appearance - ).expandRollMessage, - rollSections = html.querySelectorAll('.roll-part'), - itemDesc = html.querySelector('.domain-card-move'); - rollSections.forEach(s => { - if (s.classList.contains('roll-section')) { - const toExpand = s.querySelector('[data-action="expandRoll"]'); - toExpand.classList.toggle('expanded', autoExpandRoll.roll); - } else if (s.classList.contains('damage-section')) - s.classList.toggle('expanded', autoExpandRoll.damage); - else if (s.classList.contains('target-section')) s.classList.toggle('expanded', autoExpandRoll.target); - else if (s.classList.contains('description-section')) - s.classList.toggle('expanded', autoExpandRoll.desc); - }); - if (itemDesc && autoExpandRoll.desc) itemDesc.setAttribute('open', ''); + CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance + ).expandRollMessage; + for (const { selector, key } of DhpChatMessage.#EXPAND_SECTIONS) { + const elements = html.querySelectorAll(selector); + for (const element of elements) { + element.classList.toggle('expanded', autoExpandRoll[key]); + } + } + + // Auto expand the item description. These are not preserved by foundry during re-renders + const itemDesc = html.querySelector('details'); + if (itemDesc) { + const existing = document.querySelector(`.chat-message[data-message-id="${this.id}"] details`); + if (existing?.hasAttribute('open') ?? autoExpandRoll.desc) { + itemDesc.setAttribute('open', ''); + } + } } if (!this.isAuthor && !this.speakerActor?.isOwner) { From 0c2d25787182f6ae9145ae0f8e4ba7592320e793 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Tue, 14 Jul 2026 14:36:56 +0200 Subject: [PATCH 15/26] Raised version --- system.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system.json b/system.json index 37242137..93b1dea7 100644 --- a/system.json +++ b/system.json @@ -2,7 +2,7 @@ "id": "daggerheart", "title": "Daggerheart", "description": "An unofficial implementation of the Daggerheart system", - "version": "2.5.3", + "version": "2.5.4", "compatibility": { "minimum": "14.364", "verified": "14.364", @@ -10,7 +10,7 @@ }, "url": "https://github.com/Foundryborne/daggerheart", "manifest": "https://raw.githubusercontent.com/Foundryborne/daggerheart/v14/system.json", - "download": "https://github.com/Foundryborne/daggerheart/releases/download/2.5.3/system.zip", + "download": "https://github.com/Foundryborne/daggerheart/releases/download/2.5.4/system.zip", "authors": [ { "name": "WBHarry" From 79d652261459eeed358ff597154e36c7e8fa9cd6 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Tue, 14 Jul 2026 08:39:53 -0400 Subject: [PATCH 16/26] [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 17/26] 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 18/26] 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 19/26] 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 20/26] [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 21/26] 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 22/26] 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 23/26] [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"}}
    From d50545af4ecc95e749e8f2bc334cffd48dbe6b97 Mon Sep 17 00:00:00 2001 From: Murilo Brito <91566541+moliloo@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:50:03 -0300 Subject: [PATCH 24/26] [Rework] Fear Tracker (#2097) * rework fear tracker application, add static positions to hold the tracker, enhance token and bar display --- lang/en.json | 10 + module/applications/ui/chatLog.mjs | 6 + module/applications/ui/fearTracker.mjs | 203 ++++++++++++- module/config/generalConfig.mjs | 8 + module/data/settings/Appearance.mjs | 5 + styles/less/ui/countdown/countdown.less | 3 +- styles/less/ui/resources/resources.less | 284 ++++++++++++------ .../settings/appearance-settings/main.hbs | 4 + templates/ui/fearTracker.hbs | 46 ++- 9 files changed, 457 insertions(+), 112 deletions(-) diff --git a/lang/en.json b/lang/en.json index d0b8d0f7..7aa87c7a 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2699,6 +2699,9 @@ "displayFear": { "label": "Display Fear" }, + "fearPosition": { + "label": "Fear Position" + }, "displayCountdownUI": { "label": "Display Countdown UI" }, @@ -2750,6 +2753,13 @@ "token": "Tokens", "bar": "Bar", "hide": "Hide" + }, + "fearPosition": { + "free": "Free", + "topCenter": "Top + Center", + "bottomCenter": "Bottom + Center", + "rightTop": "Right + Top", + "leftBottom": "Left + Bottom" } }, "Automation": { diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index 0ad436ec..1ed4607f 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -1,6 +1,7 @@ import { enrichedDualityRoll } from '../../enrichers/DualityRollEnricher.mjs'; import { enrichedFateRoll, getFateTypeData } from '../../enrichers/FateRollEnricher.mjs'; import { getCommandTarget, rollCommandToJSON } from '../../helpers/utils.mjs'; +import FearTracker from './fearTracker.mjs'; export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog { constructor(options) { @@ -279,6 +280,11 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo new game.system.api.applications.dialogs.RiskItAllDialog(actor, resourceValue).render({ force: true }); } + _toggleNotifications({ closing = false } = {}) { + super._toggleNotifications(closing) + FearTracker.handleOffSet(); + } + async onRollReloadCheck(_event, messageData) { const message = game.messages.get(messageData._id); const needsReload = await message.system.action.handleReload?.({ awaitRoll: true }); diff --git a/module/applications/ui/fearTracker.mjs b/module/applications/ui/fearTracker.mjs index 8c247f79..d1813736 100644 --- a/module/applications/ui/fearTracker.mjs +++ b/module/applications/ui/fearTracker.mjs @@ -13,6 +13,14 @@ const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV2) { constructor(options = {}) { super(options); + + this._dragData = { + isDragging: false, + startX: 0, + startY: 0, + startLeft: 0, + startTop: 0 + } } /** @inheritDoc */ @@ -21,19 +29,20 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV classes: [], tag: 'div', window: { - frame: true, + frame: false, title: 'DAGGERHEART.GENERAL.fear', positioned: true, resizable: true, minimizable: false }, + classes: ['daggerheart', 'dh-style', 'fear-tracker'], actions: { setFear: FearTracker.setFear, increaseFear: FearTracker.increaseFear }, position: { - width: 222, - height: 222 + width: 540, + height: 'auto' } }; @@ -53,6 +62,10 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV return game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear; } + get fearPosition() { + return game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).fearPosition; + } + /* -------------------------------------------- */ /* Rendering */ /* -------------------------------------------- */ @@ -63,15 +76,48 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV current = this.currentFear, max = this.maxFear, percent = (current / max) * 100, - isGM = game.user.isGM; + isGM = game.user.isGM, + locked = false, + isFree = this.fearPosition == 'free'; - return { display, current, max, percent, isGM }; + return { display, current, max, percent, isGM, locked, isFree }; } /** @override */ - async _preFirstRender(context, options) { - options.position = - game.user.getFlag(CONFIG.DH.id, 'app.resources.position') ?? FearTracker.DEFAULT_OPTIONS.position; + async _onRender(context, options) { + await super._onRender(context, options); + + this.#setupDragging(); + this.#setupResizing(); + + const fearPosition = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).fearPosition; + + if (options.isFirstRender) FearTracker.handleOffSet(); + if (!options.force) return; + + this.handleStyleElement(fearPosition); + + switch (fearPosition) { + case 'topCenter': + document.getElementById('ui-top')?.appendChild(this.element); + break; + case 'bottomCenter': + document.getElementById('ui-bottom')?.prepend(this.element); + break; + case 'rightTop': + document.getElementById('ui-right-column-1')?.appendChild(this.element); + break; + case 'leftBottom': + document.getElementById('ui-left-column-1')?.insertBefore(this.element, document.getElementById('players')); + break; + + default: + document.body?.appendChild(this.element); + const position = + game.user.getFlag(CONFIG.DH.id, 'app.resources.position') ?? FearTracker.DEFAULT_OPTIONS.position; + this.setPosition(position); + break; + } } /** @override */ @@ -80,13 +126,29 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, this.maxFear); } - _onPosition(position) { - game.user.setFlag(CONFIG.DH.id, 'app.resources.position', position); + handleStyleElement(fearPosition) { + for (const position of Object.values(CONFIG.DH.GENERAL.fearPosition)) { + this.element.classList.remove(position.value); + } + + this.element.classList.add(fearPosition); } - async close(options = {}) { - if (!options.allowed) return; - else super.close(options); + static handleOffSet() { + const fearTracker = document.getElementById('resources'); + const hotbar = document.getElementById('hotbar'); + + if (!fearTracker) return; + + const offset = Number(hotbar.style.getPropertyValue('--offset').replace(/px$/, '')) || 0; + + if (offset > 0) return; + + fearTracker.style.setProperty('--offset', `${offset - 13}px`); + } + + _onPosition(position) { + game.user.setFlag(CONFIG.DH.id, 'app.resources.position', position); } static async setFear(event, target) { @@ -110,4 +172,119 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV value ); } + + // TODO: Remove methods later to use Foundry's dragger and resize methods + /* -------------------------------------------- */ + /* Dragging handlers */ + /* -------------------------------------------- */ + #setupDragging() { + const dragHandle = this.element.querySelector('.drag-handle'); + if (!dragHandle) return; + dragHandle.addEventListener('mousedown', this.#onDragStart.bind(this)); + } + + #onDragStart(event) { + if (event.button !== 0) return; + this._dragData.isDragging = true; + this._dragData.startX = event.clientX; + this._dragData.startY = event.clientY; + const rect = this.element.getBoundingClientRect(); + this._dragData.startLeft = rect.left; + this._dragData.startTop = rect.top; + this.element.style.cursor = 'grabbing'; + + this._dragHandler = this.#onDragging.bind(this); + this._dragEndHandler = this.#onDragEnd.bind(this); + window.addEventListener('mousemove', this._dragHandler); + window.addEventListener('mouseup', this._dragEndHandler); + } + + #onDragging(event) { + if (!this._dragData.isDragging) return; + + const dragX = event.clientX - this._dragData.startX; + const dragY = event.clientY - this._dragData.startY; + + this.element.style.left = `${this._dragData.startLeft + dragX}px`; + this.element.style.top = `${this._dragData.startTop + dragY}px`; + } + + #onDragEnd() { + if (!this._dragData.isDragging) return; + this._dragData.isDragging = false; + this.element.style.cursor = ''; + + if (this._dragHandler) window.removeEventListener('mousemove', this._dragHandler); + if (this._dragEndHandler) window.removeEventListener('mouseup', this._dragEndHandler); + + const rect = this.element.getBoundingClientRect(); + const pos = { top: rect.top, left: rect.left }; + + this.setPosition(pos); + } + + /* -------------------------------------------- */ + /* Resize handlers */ + /* -------------------------------------------- */ + + #setupResizing() { + const resizeHandle = this.element.querySelector('.resize-handle'); + if (!resizeHandle) return; + resizeHandle.addEventListener('mousedown', this.#onResizeStart.bind(this)); + } + + #onResizeStart(e) { + if (e.button !== 0) return; + e.stopPropagation(); + + let maxAllowedWidth = 10000; + + this._resizeData = { + isResizing: true, + startX: e.clientX, + startY: e.clientY, + startWidth: this.element.offsetWidth, + startHeight: this.element.offsetHeight, + maxAllowedWidth: Math.max(50, maxAllowedWidth) + }; + + this._resizeHandler = this.#onResizing.bind(this); + this._resizeEndHandler = this.#onResizeEnd.bind(this); + window.addEventListener('mousemove', this._resizeHandler); + window.addEventListener('mouseup', this._resizeEndHandler); + } + + #onResizing(e) { + if (!this._resizeData?.isResizing) return; + + const currentDx = e.clientX - this._resizeData.startX; + const potentialWidth = Math.max(50, this._resizeData.startWidth + currentDx); + + const width = Math.min(potentialWidth, this._resizeData.maxAllowedWidth); + + this.element.style.width = `${width}px`; + + if (width < 100) { + this.element.classList.add('narrow'); + } else { + this.element.classList.remove('narrow'); + } + } + + #onResizeEnd() { + if (!this._resizeData?.isResizing) return; + this._resizeData.isResizing = false; + + if (this._resizeHandler) window.removeEventListener('mousemove', this._resizeHandler); + if (this._resizeEndHandler) window.removeEventListener('mouseup', this._resizeEndHandler); + + let width = parseFloat(this.element.style.width); + + + if (isNaN(width)) { + width = this.element.getBoundingClientRect().width; + } + + this.setPosition({ width: width }); + } } diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 802f5907..3876f6b5 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -924,6 +924,14 @@ export const fearDisplay = { hide: { value: 'hide', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.hide' } }; +export const fearPosition = { + free: { value: 'free', label: 'DAGGERHEART.SETTINGS.Appearance.fearPosition.free' }, + topCenter: { value: 'topCenter', label: 'DAGGERHEART.SETTINGS.Appearance.fearPosition.topCenter' }, + bottomCenter: { value: 'bottomCenter', label: 'DAGGERHEART.SETTINGS.Appearance.fearPosition.bottomCenter' }, + rightTop: { value: 'rightTop', label: 'DAGGERHEART.SETTINGS.Appearance.fearPosition.rightTop' }, + leftBottom: { value: 'leftBottom', label: 'DAGGERHEART.SETTINGS.Appearance.fearPosition.leftBottom' } +}; + export const basicOwnershiplevels = { 0: { value: 0, label: 'OWNERSHIP.NONE' }, 2: { value: 2, label: 'OWNERSHIP.OBSERVER' }, diff --git a/module/data/settings/Appearance.mjs b/module/data/settings/Appearance.mjs index 4db27be0..16f5fac0 100644 --- a/module/data/settings/Appearance.mjs +++ b/module/data/settings/Appearance.mjs @@ -41,6 +41,11 @@ export default class DhAppearance extends foundry.abstract.DataModel { choices: CONFIG.DH.GENERAL.fearDisplay, initial: CONFIG.DH.GENERAL.fearDisplay.token.value }), + fearPosition: new StringField({ + required: true, + choices: CONFIG.DH.GENERAL.fearPosition, + initial: CONFIG.DH.GENERAL.fearPosition.topCenter.value + }), displayCountdownUI: new BooleanField({ initial: true }), diceSoNice: new SchemaField({ hope: diceStyle({ fg: '#ffffff', bg: '#ffe760', outline: '#000000', edge: '#ffffff' }), diff --git a/styles/less/ui/countdown/countdown.less b/styles/less/ui/countdown/countdown.less index 96e01ffd..4e64d50a 100644 --- a/styles/less/ui/countdown/countdown.less +++ b/styles/less/ui/countdown/countdown.less @@ -32,7 +32,7 @@ background: var(--background); border-radius: 4px; opacity: var(--ui-fade-opacity); - transition: opacity var(--ui-fade-duration); + transition: opacity var(--ui-fade-delay) var(--ui-fade-duration); } &:not(.performance-low, .noblur) { @@ -41,6 +41,7 @@ &:hover::before { opacity: 1; + transition: opacity var(--ui-fade-duration); } #ui-right:has(#effects-display .effect-container) & { diff --git a/styles/less/ui/resources/resources.less b/styles/less/ui/resources/resources.less index 3982d990..27f11942 100644 --- a/styles/less/ui/resources/resources.less +++ b/styles/less/ui/resources/resources.less @@ -1,119 +1,221 @@ :root { - --shadow-text-stroke: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; - --fear-animation: background 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease, opacity 0.3s ease; + --hotbar-size: 60px; +} + +#interface.theme-dark, +body.theme-dark { + .daggerheart.dh-style.fear-tracker { + --background: url(../assets/parchments/dh-parchment-dark.png); + } +} + +#interface.theme-light, +body.theme-light { + .daggerheart.dh-style.fear-tracker { + --background: url('../assets/parchments/dh-parchment-light.png') no-repeat center; + } +} + +#ui-middle:has(#hotbar.sm), +#ui-middle:has(#hotbar.md.offset) { + #resources { + &.top-center, + &.bottom-center { + width: calc((var(--hotbar-size) * 5) + 32px) !important; + } + } } #resources { + position: static; min-height: calc(var(--header-height) + 4rem); min-width: 4rem; color: #d3d3d3; - transition: var(--fear-animation); - header, - .controls, - .window-resize-handle { - transition: var(--fear-animation); + pointer-events: all; + padding: var(--spacer-8); + max-width: 540px; + min-width: 100px; + + &::before { + content: ' '; + position: absolute; + inset: 0; + background: var(--background); + border-radius: 8px; + opacity: var(--ui-fade-opacity); + transition: opacity var(--ui-fade-delay) var(--ui-fade-duration); + } - .window-content { - padding: 0.5rem; - #resource-fear { + + &:hover::before { + opacity: 1; + transition: opacity var(--ui-fade-duration); + } + + &.free { + position: absolute; + } + + &.topCenter, + &.bottomCenter { + margin: 1rem 0; + width: 100% !important; + transform: translateX(var(--offset)); + transition: all 250ms ease; + } + + &.rightTop { + width: 300px !important; + max-width: 300px; + } + + &.leftBottom { + width: 200px !important; + max-width: 200px; + background: transparent; + } + + &:not(.performance-low, .noblur) { + backdrop-filter: blur(5px); + } + + #ui-right:has(#effects-display .effect-container) & { + right: 62px; + } + + #resource-fear { + position: relative; + + &:hover { + .fear-header { + opacity: 1; + height: 18.75px; + visibility: visible; + } + + .resize-handle { + opacity: 1; + } + } + + .fear-header { + display: flex; + gap: 5px; + pointer-events: all; + margin-bottom: 0.5rem; + opacity: 0; + height: 0; + visibility: hidden; + transition: all 0.3s ease; + + .drag-handle { + cursor: grab; + } + + .fear-title { + font-size: var(--font-size-13); + } + } + + .fear-tokens { display: flex; flex-direction: row; - gap: 0.5rem 0.25rem; flex-wrap: wrap; - i { - font-size: var(--font-size-18); - border: 1px solid rgba(0, 0, 0, 0.5); + justify-content: center; + gap: 0.5rem 0.25rem; + + .fear-token { + font-size: var(--font-size-16); + border: 1.5px double light-dark(@dark-15, @dark-golden-80); border-radius: 50%; aspect-ratio: 1; display: flex; justify-content: center; align-items: center; - width: 3rem; + width: 2.5rem; background-color: @primary-color-fear; - -webkit-box-shadow: 0px 0px 5px 1px rgba(0, 0, 0, 0.75); - box-shadow: 0px 0px 5px 1px rgba(0, 0, 0, 0.75); - color: #d3d3d3; + color: @beige; flex-grow: 0; + text-shadow: none; + &.inactive { filter: grayscale(1) !important; opacity: 0.5; } } - .controls, - .resource-bar { - border: 2px solid rgb(153 122 79); - background-color: rgb(24 22 46); - } - .controls { - display: flex; - align-self: center; - border-radius: 50%; - align-items: center; - justify-content: center; - width: 30px; - height: 30px; - font-size: var(--font-size-20); - cursor: pointer; - &:hover { - font-size: 1.5rem; - } - &.disabled { - opacity: 0.5; - } - } - .resource-bar { - display: flex; - justify-content: center; - border-radius: 6px; - font-size: var(--font-size-20); - overflow: hidden; - position: relative; - padding: 0.25rem 0.5rem; - flex: 1; - text-shadow: var(--shadow-text-stroke); - &:before { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - width: var(--fear-percent); - max-width: 100%; - background: linear-gradient(90deg, rgba(2, 0, 38, 1) 0%, rgba(199, 1, 252, 1) 100%); - z-index: 0; - border-radius: 4px; - } - span { - position: inherit; - z-index: 1; - } - &.fear { - } - } - &.isGM { - i { - cursor: pointer; - &:hover { - font-size: var(--font-size-20); - } - } - } } - } - button[data-action='close'] { - display: none; - } - &:not(:hover):not(.minimized) { - background: transparent; - box-shadow: unset; - border-color: transparent; - header, - #resource-fear .controls, - .window-resize-handle { + + .resize-handle { + position: absolute; + bottom: -12px; + right: -9px; + cursor: nwse-resize; opacity: 0; + transition: all 0.3s ease; + } + + + .resource-bar { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + height: 30px; + width: 100%; + + .progress-bar { + position: absolute; + appearance: none; + width: 100%; + height: 100%; + border: 1px solid light-dark(@dark-15, @dark-golden-80); + border-radius: 999px; + z-index: 0; + background: @dark-blue; + + &::-webkit-progress-bar { + border: none; + background: @dark-blue; + border-radius: 999px; + } + &::-webkit-progress-value { + background: linear-gradient(90deg, rgba(2, 0, 38, 1) 0%, rgba(199, 1, 252, 1) 100%); + border-radius: 999px; + } + } + + .label { + margin: auto 0; + z-index: 2; + height: auto; + text-align: center; + font-size: var(--font-size-18); + color: @beige; + } + } + + .controls { + display: flex; + gap: 8px; + justify-content: center; + align-items: center; + margin-top: 0.5rem; + color: light-dark(@dark, @beige); + + .disabled { + opacity: 0.5; + } + } + + &.isGM { + .fear-token { + cursor: pointer; + transition: box-shadow 0.15s ease; + + &:hover { + box-shadow: 0 0 8px @primary-color-fear ; + } + } } - } - &:has(.fear-bar) { - min-width: 200px; } } diff --git a/templates/settings/appearance-settings/main.hbs b/templates/settings/appearance-settings/main.hbs index 32dd9e63..324ea653 100644 --- a/templates/settings/appearance-settings/main.hbs +++ b/templates/settings/appearance-settings/main.hbs @@ -8,6 +8,10 @@ value=setting.displayFear localize=true}} {{formGroup + fields.fearPosition + value=setting.fearPosition + localize=true}} + {{formGroup fields.displayCountdownUI value=setting.displayCountdownUI localize=true}} diff --git a/templates/ui/fearTracker.hbs b/templates/ui/fearTracker.hbs index 6832ab90..deae762a 100644 --- a/templates/ui/fearTracker.hbs +++ b/templates/ui/fearTracker.hbs @@ -1,16 +1,48 @@
    -
    +
    + {{#if isFree}} +
    +
    + +
    + {{localize 'DAGGERHEART.GENERAL.fear'}} +
    + {{/if}} + {{#if (eq display 'token')}} - {{#times max}} - - {{/times}} +
    + {{#times max}} + + + + {{/times}} +
    {{/if}} {{#if (eq display 'bar')}} - {{#if isGM}}
    -
    {{/if}}
    - {{current}}/{{max}} + +

    {{current}} / {{max}}

    - {{#if isGM}}
    +
    {{/if}} + {{#if isGM}} +
    + + +
    + {{/if}} + {{/if}} + + {{#if isFree}} + + + {{/if}}
    \ No newline at end of file From 7f61715adf555ec724ac2540abfb0633b825c7d1 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Wed, 22 Jul 2026 03:36:28 +0200 Subject: [PATCH 25/26] [Fix] Reload Fixes (#2099) --- lang/en.json | 11 ++- module/applications/ui/chatLog.mjs | 16 ++++- module/config/settingsConfig.mjs | 6 +- module/data/action/attackAction.mjs | 8 +-- module/data/chat-message/actorRoll.mjs | 19 +++-- module/data/settings/Automation.mjs | 2 +- module/dice/dhRoll.mjs | 8 ++- styles/less/ui/chat/chat.less | 96 ++++++++++++++++--------- templates/ui/chat/parts/button-part.hbs | 51 ++++++++----- templates/ui/chat/roll.hbs | 10 --- 10 files changed, 150 insertions(+), 77 deletions(-) diff --git a/lang/en.json b/lang/en.json index 7aa87c7a..232315c2 100755 --- a/lang/en.json +++ b/lang/en.json @@ -128,7 +128,12 @@ }, "Reload": { "checkReload": "Check Reload", - "reloadRequired": "Reload Required!" + "reloadRequired": "Reload Required!", + "notRolled": "Not Rolled", + "checkFailed": "Check Failed", + "checkPassed": "Check Passed", + "rerollConfirmationTitle": "Reroll Reload Check", + "rerollConfirmationText": "Are you sure you want to reload the reload check?" }, "RollField": { "diceRolling": { @@ -1328,7 +1333,7 @@ }, "ReloadChoices": { "off": { "label": "Don't Use" }, - "button": { "label": "Use button" }, + "manual": { "label": "Manual" }, "auto": { "label": "Automatic" } }, "RollTypes": { @@ -2820,7 +2825,7 @@ }, "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" + "hint": "If the system should automatically roll reload checks or wait for the manual press of a button in chat" }, "resourceScrollTexts": { "label": "Show Resource Change Scrolltexts", diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index 1ed4607f..5fb61857 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -287,7 +287,19 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo 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 }); + + if (message.system.reloadCheckValue) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: _loc('DAGGERHEART.ACTIONS.Reload.rerollConfirmationTitle') + }, + content: _loc('DAGGERHEART.ACTIONS.Reload.rerollConfirmationText') + }); + + if (!confirmed) return; + } + + const { rollValue } = await message.system.action.handleReload?.({ awaitRoll: true }); + await message.update({ 'system.reloadCheckValue': rollValue }); } } diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index daa89959..92a3361c 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -68,9 +68,9 @@ export const reloadChoices = { id: 'off', label: 'DAGGERHEART.CONFIG.ReloadChoices.off.label' }, - button: { - id: 'button', - label: 'DAGGERHEART.CONFIG.ReloadChoices.button.label' + manual: { + id: 'manual', + label: 'DAGGERHEART.CONFIG.ReloadChoices.manual.label' }, auto: { id: 'auto', diff --git a/module/data/action/attackAction.mjs b/module/data/action/attackAction.mjs index bf78520d..ba11c6ff 100644 --- a/module/data/action/attackAction.mjs +++ b/module/data/action/attackAction.mjs @@ -74,13 +74,13 @@ export default class DHAttackAction extends DHDamageAction { else game.dice3d.showForRoll(roll, game.user, true); } - - const needsToReload = roll.total === 1; - if (needsToReload) { + + const needsReload = roll.total === 1; + if (needsReload) { this.item.update({ 'system.resource.value': 0 }); } - return needsToReload; + return { needsReload, rollValue: roll.total }; } /** diff --git a/module/data/chat-message/actorRoll.mjs b/module/data/chat-message/actorRoll.mjs index 7071ed1e..3dcf2a8d 100644 --- a/module/data/chat-message/actorRoll.mjs +++ b/module/data/chat-message/actorRoll.mjs @@ -42,7 +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 }), + reloadCheckValue: new fields.NumberField({ integer: true, nullable: true, initial: null }), isDirect: new fields.BooleanField({ initial: false }), onSave: new fields.StringField(), source: new fields.SchemaField({ @@ -76,10 +76,14 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { return fromUuidSync(this.source.actor); } - get actionItem() { + get item() { const actionActor = this.actionActor; if (!actionActor || !this.source.item) return null; + + return actionActor.items.get(this.source.item); + } + get actionItem() { switch (this.source.originItem.type) { case CONFIG.DH.ITEM.originItemType.restMove: const restMoves = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).restMoves; @@ -87,11 +91,18 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { this.source.originItem.actionIndex ]; default: - const item = actionActor.items.get(this.source.item); - return item ? item.system.actionsList?.find(a => a.id === this.source.action) : null; + return this.item?.system.actionsList?.find(a => a.id === this.source.action); } } + get hasReload() { + return this.item?.system.hasReload; + } + + get reloadCheckFailed() { + return this.reloadCheckValue === 1; + } + get action() { const { actionActor, actionItem: itemAction } = this; if (!this.source.action) return null; diff --git a/module/data/settings/Automation.mjs b/module/data/settings/Automation.mjs index e7bc0458..523ddaac 100644 --- a/module/data/settings/Automation.mjs +++ b/module/data/settings/Automation.mjs @@ -199,7 +199,7 @@ 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, + initial: CONFIG.DH.SETTINGS.reloadChoices.manual.id, label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.reload.label', hint: 'DAGGERHEART.SETTINGS.Automation.FIELDS.reload.hint' }), diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index 62d9ff9f..68185984 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -139,7 +139,7 @@ export default class DHRoll extends BaseRoll { item?.system.hasReload && action?.type === 'attack' && reloadSetting === CONFIG.DH.SETTINGS.reloadChoices.auto.id; - const needsReload = useReload ? await action?.handleReload?.() : false; + const reloadResult = useReload ? await action?.handleReload?.() : {}; const cls = getDocumentClass('ChatMessage'), msgData = { @@ -148,7 +148,11 @@ 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, needsReload }, + system: { + ...config, + actionDescription, + reloadCheckValue: reloadResult.rollValue + }, rolls: [roll] }; diff --git a/styles/less/ui/chat/chat.less b/styles/less/ui/chat/chat.less index 8c3f4b08..1b18a823 100644 --- a/styles/less/ui/chat/chat.less +++ b/styles/less/ui/chat/chat.less @@ -271,28 +271,6 @@ } } - .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; @@ -643,20 +621,74 @@ } .roll-buttons { - display: flex; - flex-wrap: wrap; - gap: 5px; - width: 100%; margin-top: 8px; + display: flex; + flex-direction: column; + gap: 5px; button { - height: 36px; - flex: 1 1 calc(50% - 5px); - font-family: @font-body; - font-weight: 700; + height: 32px; + } - &:nth-last-child(1):nth-child(odd) { - flex-basis: 100%; + .main-buttons { + display: flex; + gap: 5px; + + button { + flex: 1; + + &.end-button { + flex: 0; + } + } + } + + .extra-button-row { + width: 100%; + display: flex; + gap: 5px; + + .image-container { + position: relative; + display: flex; + align-items: center; + justify-content: center; + + img { + height: 24px; + filter: @dark-filter; + } + + .dice-result { + position: absolute; + font-size: 18px; + color: @beige; + filter: drop-shadow(0 0 1px @light-white); + } + } + + .reload-data { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + border-radius: 5px; + gap: 5px; + cursor: pointer; + padding: 5px; + transition: all 0.3s ease; + color: @golden; + background: @golden-40; + + &.failed-check { + background: @red-40; + color: @red; + } + + &.passed-check { + background: @green-40; + color: @green; + } } } } diff --git a/templates/ui/chat/parts/button-part.hbs b/templates/ui/chat/parts/button-part.hbs index b631192c..6dc39804 100644 --- a/templates/ui/chat/parts/button-part.hbs +++ b/templates/ui/chat/parts/button-part.hbs @@ -1,21 +1,40 @@
    - {{#if (eq automationSettings.reload 'button')}} - - {{/if}} - {{#if areas.length}}{{/if}} - {{#if hasDamage}} - {{#if damage.active}} - - {{else}} - +
    + {{#if areas.length}}{{/if}} + {{#if hasDamage}} + {{#if damage.active}} + + {{else}} + + {{/if}} {{/if}} - {{/if}} - {{#if hasHealing}} - {{#if damage.active}} - - {{else}} - + {{#if hasHealing}} + {{#if damage.active}} + + {{else}} + + {{/if}} {{/if}} + {{#if (and hasEffect)}}{{/if}} +
    + + {{#if parent.system.hasReload}} +
    + +
    + {{#unless reloadCheckValue}} + {{localize "DAGGERHEART.ACTIONS.Reload.notRolled"}} + {{else}} + {{ifThen parent.system.reloadCheckFailed (localize "DAGGERHEART.ACTIONS.Reload.checkFailed") (localize "DAGGERHEART.ACTIONS.Reload.checkPassed")}} + {{/unless}} +
    +
    {{/if}} - {{#if (and hasEffect)}}{{/if}}
    \ No newline at end of file diff --git a/templates/ui/chat/roll.hbs b/templates/ui/chat/roll.hbs index ab4e197e..362aeadd 100644 --- a/templates/ui/chat/roll.hbs +++ b/templates/ui/chat/roll.hbs @@ -1,15 +1,5 @@
    {{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}} From a9ac8fe148204f50c7cfd29525b11272f9d2fde8 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Wed, 22 Jul 2026 03:44:10 +0200 Subject: [PATCH 26/26] Fixed so that non attack/damage actions don't error out when used (#2098) --- module/data/action/baseAction.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index 05890335..7c783f05 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -429,11 +429,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel } get hasDamage() { - return this.type !== 'healing' && (Boolean(this.damage.main) || !foundry.utils.isEmpty(this.damage.resources)); + return this.type !== 'healing' && (Boolean(this.damage?.main) || !foundry.utils.isEmpty(this.damage?.resources)); } get hasHealing() { - return this.type === 'healing' && !foundry.utils.isEmpty(this.damage.resources); + return this.type === 'healing' && !foundry.utils.isEmpty(this.damage?.resources); } get hasSave() {