From 3faf588e6c361555d6985b2e94574b3ea89db557 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Sun, 12 Jul 2026 20:43:56 -0400 Subject: [PATCH 01/17] 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 02/17] 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 03/17] 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 04/17] 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 05/17] 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 06/17] 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 07/17] [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 08/17] 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 09/17] 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 10/17] 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 11/17] 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 12/17] [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 13/17] 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 14/17] 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 15/17] 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 3b68c6c8952815d06d70cd6af0a6ce954adbbff2 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:31:38 +0200 Subject: [PATCH 16/17] [Rework] Damage and Damage Resource split (#2094) --- module/applications/dialogs/damageDialog.mjs | 12 +- module/applications/dialogs/tagTeamDialog.mjs | 94 ++++++--- .../sheets-configs/action-base-config.mjs | 98 +++++---- .../sheets-configs/adversary-settings.mjs | 35 +++- module/applications/ui/chatLog.mjs | 18 +- 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 | 59 ++++-- module/data/chat-message/chatDamageData.mjs | 30 +-- module/data/fields/action/damageField.mjs | 81 ++++---- module/data/item/weapon.mjs | 23 +-- module/dice/damageRoll.mjs | 126 +++++++----- module/dice/dhRoll.mjs | 6 +- module/documents/actor.mjs | 107 +++++----- module/helpers/handlebarsHelper.mjs | 5 +- module/helpers/utils.mjs | 13 -- .../migration-handlers/2_5_2.mjs | 3 +- .../less/dialog/damage-selection/sheet.less | 7 + templates/actionTypes/damage.hbs | 194 ++++++++++-------- .../dialogs/dice-roll/damageSelection.hbs | 69 ++++--- .../parts/tagTeamDamageParts.hbs | 16 +- .../action-settings/effect.hbs | 2 +- .../adversary-settings/attack.hbs | 2 +- templates/sheets/items/weapon/settings.hbs | 40 ++-- templates/ui/chat/parts/damage-part.hbs | 111 +++++----- templates/ui/tooltip/attack.hbs | 2 +- templates/ui/tooltip/weapon.hbs | 2 +- 31 files changed, 730 insertions(+), 531 deletions(-) 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 c54720f4..4c6c5339 100644 --- a/module/applications/dialogs/tagTeamDialog.mjs +++ b/module/applications/dialogs/tagTeamDialog.mjs @@ -553,23 +553,27 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio const { memberKey } = button.dataset; this.updatePartyData( { - [`system.tagTeam.members.${memberKey}.damageRollData.types`]: - _replace({}) + [`system.tagTeam.members.${memberKey}.damageRollData`]: { + main: null, + resources: _replace({}) + } }, this.getUpdatingParts(button) ); } static async #rerollDamageDice(_, button) { - const { memberKey, damageKey, diceIndex, resultIndex } = button.dataset; + const { isResource, memberKey, damageKey, diceIndex, resultIndex } = button.dataset; const memberData = this.party.system.tagTeam.members[memberKey]; - await memberData.damageRollData.rerollDamageDie(damageKey, diceIndex, resultIndex); + 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}.damageRollData.types`]: { - [damageKey]: memberData.damageRollData.types[damageKey].toJSON() - } + [updatePath]: updateValue.toJSON() }, this.getUpdatingParts(button) ); @@ -577,19 +581,19 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio async getCriticalDamage(origDamage) { const newDamage = origDamage ? ChatDamageData.fromJSON(JSON.stringify(origDamage)) : null; - for (let key in newDamage?.types ?? {}) { - const criticalDamage = await getCritDamageBonus(newDamage.types[key].formula); - if (!criticalDamage) continue; - - const criticalTerm = new foundry.dice.terms.NumericTerm({ number: criticalDamage, evaluated: true }); - criticalTerm.evaluate(); - newDamage.types[key] = await Roll.fromTerms([ - ...origDamage.types[key].terms, - new foundry.dice.terms.OperatorTerm({ operator: '+' }), - criticalTerm - ]); - newDamage.types[key].options = foundry.utils.deepClone(origDamage.types[key].options); - } + 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; } @@ -644,25 +648,47 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio ? await this.getCriticalDamage(secondaryRoll.damageRollData) : secondaryRoll.damageRollData; if (mainRoll.damageRollData) { - for (const [key, damage] of Object.entries(secondaryDamage.types ?? {})) { - if (key in mainRoll.damageRollData.types) { - mainRoll.damageRollData.types[key] = Roll.fromTerms([ - ...baseMainRoll.damageRollData.types[key].terms, + if (secondaryDamage.main) { + if (mainRoll.damageRollData.main) { + mainRoll.damageRollData.main = Roll.fromTerms([ + ...baseMainRoll.damageRollData.main.terms, new foundry.dice.terms.OperatorTerm({ operator: '+' }), - ...baseSecondaryRoll.damageRollData.types[key].terms + ...baseSecondaryRoll.damageRollData.main.terms ]); /* Joining the roll.options of both rolls */ const joinedDamageTypes = new Set([ - ...baseMainRoll.damageRollData.types[key].options.damageTypes, - ...baseSecondaryRoll.damageRollData.types[key].options.damageTypes + ...baseMainRoll.damageRollData.main.options.damageTypes, + ...baseSecondaryRoll.damageRollData.main.options.damageTypes ]); - mainRoll.damageRollData.types[key].options = { - ...baseMainRoll.damageRollData.types[key].options, + mainRoll.damageRollData.main.options = { + ...baseMainRoll.damageRollData.main.options, damageTypes: [...joinedDamageTypes] }; } else { - mainRoll.damageRollData.types[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 { @@ -727,8 +753,12 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio ...mainRoll.options, damage: joinedRoll.damageRollData?.toJSON() }; - for (const type of Object.keys(joinedRoll.damageRollData?.types ?? {})) { - systemData.damage.types[type] = joinedRoll.damageRollData.types[type].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'), diff --git a/module/applications/sheets-configs/action-base-config.mjs b/module/applications/sheets-configs/action-base-config.mjs index b65e1cdf..920cb6d0 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; } @@ -299,53 +301,71 @@ 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.damageType'), choices, required: true - }).toFormGroup( - {}, - { - name: 'type', - localize: true, - nameAttr: 'value', - labelAttr: 'label' - } - ).outerHTML; + }).toFormGroup({}, { + name: 'type', + localize: true, + nameAttr: 'value', + labelAttr: 'label' + }).outerHTML; const callback = (_, button) => { const data = this.action.toObject(); const type = choices[button.form.elements.type.value].value; - const part = this.action.schema.fields.damage.fields.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') + /** @todo localize */ + title: 'Add Damage' }, position: { width: 300 } }); @@ -353,12 +373,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 1c462930..e3a628f2 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); @@ -251,15 +251,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, dice, result } = target.dataset; - await message.system.damage.rerollDamageDie(damageType, dice, result); + 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({ - 'system.damage.types': { - [damageType]: message.system.damage.types[damageType].toJSON() - } + [updatePath]: updateValue.toJSON() }); } else { const rerollDice = message.system.roll.dice[target.dataset.dieIndex]; 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 0b5d2678..dd81952b 100644 --- a/module/data/chat-message/actorRoll.mjs +++ b/module/data/chat-message/actorRoll.mjs @@ -137,12 +137,17 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { if (!this.damage.active) return; const rerolls = []; - const update = { system: { damage: { types: {} } } }; - for (const key of Object.keys(this.damage.types)) { - const type = this.damage.types[key]; - const reroll = await type.reroll(); + 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.types[key] = reroll.toJSON(); + update.system.damage.main = reroll.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); @@ -188,22 +193,36 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { } static migrateData(source) { - if (source.hasDamage && !source.damage.types) { - source.damage = { - types: Object.keys(source.damage).reduce((acc, key) => { - const damageData = source.damage[key]; - const oldRoll = damageData.parts[0]?.roll; - acc[key] = oldRoll ? { - ...oldRoll, - options: { - ...oldRoll.options, - damageTypes: damageData.parts[0].damageTypes ?? [] - } - } : null; + const { main, resources, ...flatDamageKeys } = source.damage ?? {}; + if (source.damage && !main && !resources) { + source.damage.main = null; + source.damage.resources = {}; - return acc; - }, {}) - }; + 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; diff --git a/module/data/chat-message/chatDamageData.mjs b/module/data/chat-message/chatDamageData.mjs index b866ddaf..404cda43 100644 --- a/module/data/chat-message/chatDamageData.mjs +++ b/module/data/chat-message/chatDamageData.mjs @@ -11,31 +11,31 @@ export class ChatDamageData extends foundry.abstract.DataModel { const fields = foundry.data.fields; return { - types: new fields.TypedObjectField(new fields.JSONField({validate: ChatDamageData.#validateRoll})) + main: new fields.JSONField({ nullable: true, validate: ChatDamageData.#validateRoll}), + resources: new fields.TypedObjectField(new fields.JSONField({validate: ChatDamageData.#validateRoll})) }; } get active() { - return Boolean(Object.keys(this.types).length); + return !!this.main || Boolean(Object.keys(this.resources).length); } static #validateRoll(rollJSON) { - const roll = JSON.parse(rollJSON); - if (!roll.evaluated) throw new Error('Roll objects added to ChatMessage documents must be evaluated'); - } - - _prepareRolls() { - for (const key of Object.keys(this.types)) { - const type = this.types[key]; - try { - this.types[key] = Roll.fromData(type); - this.types[key].options.modifierTotal = CONFIG.Dice.daggerheart.DHRoll.calculateTotalModifiers(type); - } catch {} + if (rollJSON) { + const roll = JSON.parse(rollJSON); + if (!roll.evaluated) throw new Error('Roll objects added to ChatMessage documents must be evaluated'); } } - async rerollDamageDie(damageType, dice, resultIndex) { - const reroll = this.types[damageType]; + _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(); diff --git a/module/data/fields/action/damageField.mjs b/module/data/fields/action/damageField.mjs index 83e89213..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) { @@ -111,17 +99,15 @@ export default class DamageField extends fields.SchemaField { : actor.prototypeToken; if (config.hasHealing) damagePromises.push( - actor.takeHealing(config.damage.types).then(updates => targetDamage.push({ token, updates })) + actor.takeHealing(config.damage).then(updates => targetDamage.push({ token, updates })) ); else { - const configDamage = foundry.utils.deepClone(config.damage.types); - const hpDamageMultiplier = config.actionActor?.system.rules?.attack?.damage?.hpDamageMultiplier ?? 1; - const hpDamageTakenMultiplier = actor.system.rules?.attack?.damage?.hpDamageTakenMultiplier; - if (configDamage.hitPoints) { - configDamage.hitPoints = configDamage.hitPoints.toJSON(); - configDamage.hitPoints.total = Math.ceil( - configDamage.hitPoints.total * hpDamageMultiplier * hpDamageTakenMultiplier - ); + const configDamage = config.damage.clone(); + configDamage.main &&= configDamage.main.toJSON(); + if (configDamage.main) { + const multiplier = config.actionActor?.system.rules?.attack?.damage?.hpDamageMultiplier ?? 1; + const takenMultiplier = actor.system.rules?.attack?.damage?.hpDamageTakenMultiplier; + configDamage.main.total = Math.ceil(configDamage.main.total * multiplier * takenMultiplier); } damagePromises.push( @@ -184,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( @@ -198,7 +190,8 @@ export default class DamageField extends fields.SchemaField { ); if (same) same.formula += ` + ${formula.formula}`; else formattedFormulas.push(formula); - }); + } + return formattedFormulas; } @@ -295,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, @@ -317,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/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/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index 8667d89f..683d14d8 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -13,16 +13,31 @@ export default class DamageRoll extends DHRoll { static DefaultDialog = DamageDialog; + static createRollInstance(config) { + return new this(undefined, config.data, config); + } + /** @inheritdoc */ static async buildEvaluate(roll, config = {}) { - if (config.dialog.configure === false) roll.constructFormula(config); + if (config.dialog.configure === false) roll.constructFormulas(config); - for (const roll of config.roll) { + const evaluateRoll = async roll => { await roll.roll.evaluate(); roll.roll.options = { damageTypes: roll.damageTypes ? [...roll.damageTypes] : [] }; + return roll.roll; + } - if (!config.damage?.types) config.damage = { types: {} }; - config.damage.types[roll.applyTo] = roll.roll; + 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; @@ -36,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.types) - ); + const pool = foundry.dice.terms.PoolTerm.fromRolls([ + ...(config.damage.main ? [config.damage.main] : []), + ...Object.values(config.damage.resources) + ]); diceRolls.push(Roll.fromTerms([pool])); } @@ -51,7 +67,8 @@ export default class DamageRoll extends DHRoll { if (config.source?.message) { chatMessage.update({ 'system.damage': { ...config.damage.toObject(), - types: config.damage.types + main: config.damage.main, + resources: config.damage.resources }}); } } @@ -104,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) { @@ -125,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 */ diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index 13408329..c78caa4f 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -41,6 +41,10 @@ export default class DHRoll extends BaseRoll { return config; } + static createRollInstance(config) { + return new this(config.roll.formula, config.data, config); + } + /** * @param {Partial} config * @returns {Promise} @@ -58,7 +62,7 @@ export default class DHRoll extends BaseRoll { this.temporaryModifierBuilder(config); - let roll = new this(config.roll.formula, config.data, config); + let roll = this.createRollInstance(config); if (config.dialog.configure !== false) { // Open Roll Dialog const DialogClass = config.dialog?.class ?? this.DefaultDialog; diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index d7733dd5..de55b4b7 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -656,26 +656,23 @@ export default class DhpActor extends Actor { return; } - const updates = []; - - Object.entries(damages).forEach(([key, damage]) => { - if (key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) - damage.total = this.calculateDamage(damage.total, damage.damageTypes); - const update = updates.find(u => u.key === key); - if (update) { - update.value += damage.total; - update.damageTypes.add(...new Set(damage.damageTypes)); - } else updates.push({ value: damage.total, key, damageTypes: new Set(damage.damageTypes) }); - }); + if (damages.main) { + damages.main.total = this.calculateDamage(damages.main.total, damages.main.damageTypes); + } if (Hooks.call(`${CONFIG.DH.id}.postCalculateDamage`, this, damages) === false) return null; - if (!updates.length) return; + // Convert deducted resources to a record of updates. Return if nothing to do. + const updates = Object.entries(damages.resources).map(([key, damage]) => ({ key, value: damage.total })); + if (!updates.some(u => u.value) && !damages.main) return; - const hpDamage = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id); - if (hpDamage?.value) { - hpDamage.value = this.convertDamageToThreshold(hpDamage.value); - if (this.type === 'character' && !isDirect && this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes)) { + if (damages.main) { + const hpDamage = { + value: this.convertDamageToThreshold(damages.main.total), + damageTypes: new Set(damages.main.options.damageTypes), + key: CONFIG.DH.GENERAL.healingTypes.hitPoints.id + }; + if (this.type === 'character' && !isDirect && this.#canReduceDamage(hpDamage.total, hpDamage.damageTypes)) { const armorSlotResult = await this.owner.query( 'armorSlot', { @@ -689,7 +686,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 }); } @@ -699,20 +696,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( @@ -727,12 +728,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); @@ -741,6 +740,28 @@ export default class DhpActor extends Actor { return updates; } + async takeHealing(healings) { + if (Hooks.call(`${CONFIG.DH.id}.preTakeHealing`, this, healings) === false) return null; + + const updates = Object.entries(healings.resources).map(([key, damage]) => ({ + key, + value: damage.total + })); + + 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; + } + calculateDamage(baseDamage, type) { if (this.canResist(type, 'immunity')) return 0; if (this.canResist(type, 'resistance')) baseDamage = Math.ceil(baseDamage / 2); @@ -765,30 +786,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]) => { - const update = updates.find(u => u.key === key); - if (update) update.value += healing.roll.total; - else updates.push({ value: healing.roll.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/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/damage.hbs b/templates/actionTypes/damage.hbs index 03300840..df8a71b1 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=@root.source.damage.includeBase name="damage.main.includeBase" classes="checkbox" localize=true }} + {{/if}} + {{#unless (eq @root.source.type 'healing')}} + {{formField baseFields.main.fields.direct value=source.main.direct name=(concat path "damage.main.direct") localize=true classes="checkbox"}} + {{/unless}} + {{#if (and @root.isNPC (not (eq path 'system.attack.')))}} + {{formField baseFields.main.fields.groupAttack value=source.main.groupAttack name=(concat path "damage.main.groupAttack") localize=true classes="select"}} + {{/if}} +
    + {{> damageData damage=source.main fields=fields.main.fields basePath=(concat path "damage.main")}} + {{#if horde}} + {{> hordeDamage source=source.main fields=fields.main.fields basePath=(concat path "damage.main")}} + {{/if}} + {{#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.GENERAL.Resource.plural"}} + {{/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/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/damage-part.hbs b/templates/ui/chat/parts/damage-part.hbs index d4d1c9fe..7d6d983d 100644 --- a/templates/ui/chat/parts/damage-part.hbs +++ b/templates/ui/chat/parts/damage-part.hbs @@ -10,60 +10,79 @@
    - {{#each damage.types as | roll index | }} -
    {{localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.inChatRoll')}}: {{roll.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.types 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}} -
    + {{#if damage.main}} + {{> damage label=(localize "DAGGERHEART.GENERAL.damage") roll=damage.main isDirect=isDirect }} + {{/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}} -
    -
    + {{#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 6460077c94b12fba0b2cf8c2029e27511773138e Mon Sep 17 00:00:00 2001 From: WBHarry Date: Sun, 19 Jul 2026 00:17:21 +0200 Subject: [PATCH 17/17] Changed the label of ActionSheet damage.resources to Mark Resources --- lang/en.json | 4 ++-- module/config/generalConfig.mjs | 2 +- templates/actionTypes/cost.hbs | 2 +- templates/actionTypes/damage.hbs | 2 +- templates/actionTypes/resource.hbs | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lang/en.json b/lang/en.json index 508a771d..bf5f7e70 100755 --- a/lang/en.json +++ b/lang/en.json @@ -105,7 +105,8 @@ }, "damage": { "multiplier": "Multiplier", - "flatMultiplier": "Flat Multiplier" + "flatMultiplier": "Flat Multiplier", + "markResources": "Mark Resources" }, "general": { "customFormula": "Custom Formula", @@ -2483,7 +2484,6 @@ "reroll": "Reroll", "rerolled": "Rerolled", "rerollThing": "Reroll {thing}", - "resource": "Resource", "result": { "single": "Result", "plural": "Results" 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/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 df8a71b1..f8adb628 100644 --- a/templates/actionTypes/damage.hbs +++ b/templates/actionTypes/damage.hbs @@ -39,7 +39,7 @@ {{#if (eq @root.source.type 'healing')}} {{localize "DAGGERHEART.GENERAL.healing"}} {{else}} - {{localize "DAGGERHEART.GENERAL.Resource.plural"}} + {{localize "DAGGERHEART.ACTIONS.Config.damage.markResources"}} {{/if}} {{#unless @root.allDamageTypesUsed}}{{/unless}} 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"}}