From 18687b61313531d6001c7d15c33ad5c90c220c23 Mon Sep 17 00:00:00 2001 From: Chris Ryan <73275196+chrisryan10@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:01:15 +1000 Subject: [PATCH 01/18] Fix Avalanche typo (#1098) Co-authored-by: Chris Ryan --- .../adversary_Stonewraith_3aAS2Qm3R6cgaYfE.json | 8 ++++---- .../adversary_Young_Ice_Dragon_UGPiPLJsPvMTSKEF.json | 8 ++++---- .../environment_Mountain_Pass_acMu9wJrMZZzLSTJ.json | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/packs/adversaries/adversary_Stonewraith_3aAS2Qm3R6cgaYfE.json b/src/packs/adversaries/adversary_Stonewraith_3aAS2Qm3R6cgaYfE.json index 760e1c74..0d5bd5eb 100644 --- a/src/packs/adversaries/adversary_Stonewraith_3aAS2Qm3R6cgaYfE.json +++ b/src/packs/adversaries/adversary_Stonewraith_3aAS2Qm3R6cgaYfE.json @@ -427,7 +427,7 @@ "_key": "!actors.items!3aAS2Qm3R6cgaYfE.tQgxiSS48TJ3X1Dl" }, { - "name": "Avalance Roar", + "name": "Avalanche Roar", "type": "feature", "system": { "description": "

Spend a Fear to roar while within a cave and cause a cave-in. All targets within Close range must succeed on an Agility Reaction Roll (14) or take 2d10 physical damage. The rubble can be cleared with a Progress Countdown (8).

@Template[type:emanation|range:c]

", @@ -532,12 +532,12 @@ "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.346", + "coreVersion": "13.347", "systemId": "daggerheart", "systemVersion": "0.0.1", "createdTime": 1754085059319, - "modifiedTime": 1754143365810, - "lastModifiedBy": "MQSznptE5yLT7kj8" + "modifiedTime": 1756256613353, + "lastModifiedBy": "CEZZA7TXd7uT8O2c" }, "_key": "!actors.items!3aAS2Qm3R6cgaYfE.9Z0i0uURfBMVIapJ" }, diff --git a/src/packs/adversaries/adversary_Young_Ice_Dragon_UGPiPLJsPvMTSKEF.json b/src/packs/adversaries/adversary_Young_Ice_Dragon_UGPiPLJsPvMTSKEF.json index 48b1cd14..251a1ab7 100644 --- a/src/packs/adversaries/adversary_Young_Ice_Dragon_UGPiPLJsPvMTSKEF.json +++ b/src/packs/adversaries/adversary_Young_Ice_Dragon_UGPiPLJsPvMTSKEF.json @@ -548,7 +548,7 @@ "_key": "!actors.items!UGPiPLJsPvMTSKEF.QV2ytK4b1VWF71OS" }, { - "name": "Avalance", + "name": "Avalanche", "type": "feature", "system": { "description": "

Spend a Fear to have the Dragon unleash a huge downfall of snow and ice, covering all other creatures within Far range. All targets within this area must succeed on an Instinct Reaction Roll or be buried in snow and rocks, becoming Vulnerable until they dig themselves out from the debris. For each PC that fails the reaction roll, you gain a Fear.

@Template[type:emanation|range:f]

", @@ -678,12 +678,12 @@ "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.346", + "coreVersion": "13.347", "systemId": "daggerheart", "systemVersion": "0.0.1", "createdTime": 1754131703390, - "modifiedTime": 1754131790034, - "lastModifiedBy": "MQSznptE5yLT7kj8" + "modifiedTime": 1756256581072, + "lastModifiedBy": "CEZZA7TXd7uT8O2c" }, "_key": "!actors.items!UGPiPLJsPvMTSKEF.CcRTxCDCJskiu3fI" }, diff --git a/src/packs/environments/environment_Mountain_Pass_acMu9wJrMZZzLSTJ.json b/src/packs/environments/environment_Mountain_Pass_acMu9wJrMZZzLSTJ.json index e2a31c41..8b962e9f 100644 --- a/src/packs/environments/environment_Mountain_Pass_acMu9wJrMZZzLSTJ.json +++ b/src/packs/environments/environment_Mountain_Pass_acMu9wJrMZZzLSTJ.json @@ -177,7 +177,7 @@ "_key": "!actors.items!acMu9wJrMZZzLSTJ.cIAMenvMXHPTpOFn" }, { - "name": "Avalance", + "name": "Avalanche", "type": "feature", "system": { "description": "

Spend a Fear to carve the mountain with an icy torrent, causing an avalanche. All PCs in its path must succeed on an Agility or Strength Reaction Roll or be bowled over and carried down the mountain. A PC using rope, pitons, or other climbing gear gains advantage on this roll. Targets who fail are knocked down the mountain to Far range, take 2d20 physical damage, and must mark a Stress. Targets who succeed must mark a Stress.

How do the PCs try to weather the avalanche? What approach do the characters take to fi nd one another when their companions go hurtling down the mountainside?

", @@ -252,12 +252,12 @@ "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.346", + "coreVersion": "13.347", "systemId": "daggerheart", "systemVersion": "0.0.1", "createdTime": 1754217019442, - "modifiedTime": 1754217102897, - "lastModifiedBy": "MQSznptE5yLT7kj8" + "modifiedTime": 1756256534443, + "lastModifiedBy": "CEZZA7TXd7uT8O2c" }, "_key": "!actors.items!acMu9wJrMZZzLSTJ.jkm03DXYYajsRk2j" }, From 9f2c2f1bedaa8bc957d37ae8262405f9eefd2fc2 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Wed, 27 Aug 2025 15:22:29 +0200 Subject: [PATCH 02/18] Changed so the default domaincard view is list (#1102) --- module/applications/sheets/actors/character.mjs | 6 +++--- module/config/flagsConfig.mjs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index c860e9e9..a140a7c9 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -210,7 +210,7 @@ export default class CharacterSheet extends DHBaseActorSheet { * @protected */ async _prepareLoadoutContext(context, _options) { - context.cardView = !game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsList); + context.cardView = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsCard); } /** @@ -718,8 +718,8 @@ export default class CharacterSheet extends DHBaseActorSheet { * @type {ApplicationClickAction} */ static async #toggleLoadoutView(_, button) { - const newAbilityView = button.dataset.value !== 'true'; - await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsList, newAbilityView); + const newAbilityView = button.dataset.value === 'true'; + await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsCard, newAbilityView); this.render(); } diff --git a/module/config/flagsConfig.mjs b/module/config/flagsConfig.mjs index 91712288..c2a6dff2 100644 --- a/module/config/flagsConfig.mjs +++ b/module/config/flagsConfig.mjs @@ -1,4 +1,4 @@ -export const displayDomainCardsAsList = 'displayDomainCardsAsList'; +export const displayDomainCardsAsCard = 'displayDomainCardsAsCard'; export const narrativeCountdown = { simple: 'countdown-narrative-simple', position: 'countdown-narrative-position' From 3f1e7f4f4ab5af28e1e6c9eca2c5b71c15213b1c Mon Sep 17 00:00:00 2001 From: Chris Ryan <73275196+chrisryan10@users.noreply.github.com> Date: Fri, 29 Aug 2025 23:49:33 +1000 Subject: [PATCH 03/18] Added some protection for no data supplied (#1115) Co-authored-by: Chris Ryan --- module/enrichers/DualityRollEnricher.mjs | 30 ++++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/module/enrichers/DualityRollEnricher.mjs b/module/enrichers/DualityRollEnricher.mjs index 73d4f25a..1d6404ff 100644 --- a/module/enrichers/DualityRollEnricher.mjs +++ b/module/enrichers/DualityRollEnricher.mjs @@ -9,12 +9,12 @@ export default function DhDualityRollEnricher(match, _options) { } function getDualityMessage(roll, flavor) { - const trait = roll.trait && abilities[roll.trait] ? game.i18n.localize(abilities[roll.trait].label) : null; + const trait = roll?.trait && abilities[roll.trait] ? game.i18n.localize(abilities[roll.trait].label) : null; const label = flavor ?? - (roll.trait + (roll?.trait ? game.i18n.format('DAGGERHEART.GENERAL.rollWith', { roll: trait }) - : roll.reaction + : roll?.reaction ? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll') : game.i18n.localize('DAGGERHEART.GENERAL.duality')); @@ -22,9 +22,9 @@ function getDualityMessage(roll, flavor) { ? game.i18n.localize(abilities[roll.trait].label) : game.i18n.localize('DAGGERHEART.GENERAL.duality'); - const advantage = roll.advantage + const advantage = roll?.advantage ? CONFIG.DH.ACTIONS.advantageState.advantage.value - : roll.disadvantage + : roll?.disadvantage ? CONFIG.DH.ACTIONS.advantageState.disadvantage.value : undefined; const advantageLabel = @@ -36,21 +36,21 @@ function getDualityMessage(roll, flavor) { const dualityElement = document.createElement('span'); dualityElement.innerHTML = ` - `; From ef4d37d72508ecb9e0f66f23882becfdb42401cf Mon Sep 17 00:00:00 2001 From: Chris Ryan <73275196+chrisryan10@users.noreply.github.com> Date: Sat, 30 Aug 2025 11:47:08 +1000 Subject: [PATCH 04/18] typo fixes (#1121) Co-authored-by: Chris Ryan --- .../adversary_Acid_Burrower_89yAh30vaNQOALlz.json | 12 ++++++------ ...ary_Greater_Earth_Elemental_dsfB3YhoL5SudvS2.json | 12 ++++++------ .../adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json | 12 ++++++------ .../adversary_Tiny_Green_Ooze_aLkLFuVoKz2NLoBK.json | 12 ++++++------ 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/packs/adversaries/adversary_Acid_Burrower_89yAh30vaNQOALlz.json b/src/packs/adversaries/adversary_Acid_Burrower_89yAh30vaNQOALlz.json index e226f013..90c5950e 100644 --- a/src/packs/adversaries/adversary_Acid_Burrower_89yAh30vaNQOALlz.json +++ b/src/packs/adversaries/adversary_Acid_Burrower_89yAh30vaNQOALlz.json @@ -434,14 +434,14 @@ "_id": "UpFsnlbZkyvM2Ftv", "img": "icons/magic/acid/projectile-smoke-glowing.webp", "system": { - "description": "

Make an attack against all targets in front of the Burrower within Close range. Targets the Burrower succeeds against take 2d6 physical damage and must mark an Armor Slot without receiving its benefi ts (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP and you gain a Fear.

@Template[type:inFront|range:c]

", + "description": "

Make an attack against all targets in front of the Burrower within Close range. Targets the Burrower succeeds against take 2d6 physical damage and must mark an Armor Slot without receiving its benefits (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP and you gain a Fear.

@Template[type:inFront|range:c]

", "resource": null, "actions": { "yd10HwK6Wa3OEvv2": { "type": "attack", "_id": "yd10HwK6Wa3OEvv2", "systemPath": "actions", - "description": "

Make an attack against all targets in front of the Burrower within Close range. Targets the Burrower succeeds against take 2d6 physical damage and must mark an Armor Slot without receiving its benefi ts (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP and you gain a Fear.

@Template[type:inFront|range:c]

", + "description": "

Make an attack against all targets in front of the Burrower within Close range. Targets the Burrower succeeds against take 2d6 physical damage and must mark an Armor Slot without receiving its benefits (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP and you gain a Fear.

@Template[type:inFront|range:c]

", "chatDisplay": true, "actionType": "action", "cost": [], @@ -553,11 +553,11 @@ "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.346", + "coreVersion": "13.348", "systemId": "daggerheart", - "systemVersion": "0.0.1", - "lastModifiedBy": "MQSznptE5yLT7kj8", - "modifiedTime": 1754143653876 + "systemVersion": "1.1.2", + "lastModifiedBy": "mdk78Q6pOyHh6aBg", + "modifiedTime": 1756510879809 }, "_key": "!actors.items!89yAh30vaNQOALlz.UpFsnlbZkyvM2Ftv" }, diff --git a/src/packs/adversaries/adversary_Greater_Earth_Elemental_dsfB3YhoL5SudvS2.json b/src/packs/adversaries/adversary_Greater_Earth_Elemental_dsfB3YhoL5SudvS2.json index e519b193..d4554bf9 100644 --- a/src/packs/adversaries/adversary_Greater_Earth_Elemental_dsfB3YhoL5SudvS2.json +++ b/src/packs/adversaries/adversary_Greater_Earth_Elemental_dsfB3YhoL5SudvS2.json @@ -264,14 +264,14 @@ "name": "Crushing Blows", "type": "feature", "system": { - "description": "

When the Elemental makes a successful attack, the target must mark an Armor Slot without receiving its benefi ts (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP.

", + "description": "

When the Elemental makes a successful attack, the target must mark an Armor Slot without receiving its benefits (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP.

", "resource": null, "actions": { "0sXciTiPc30v8czv": { "type": "damage", "_id": "0sXciTiPc30v8czv", "systemPath": "actions", - "description": "

When the Elemental makes a successful attack, the target must mark an Armor Slot without receiving its benefi ts (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP.

", + "description": "

When the Elemental makes a successful attack, the target must mark an Armor Slot without receiving its benefits (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP.

", "chatDisplay": true, "actionType": "action", "cost": [], @@ -337,12 +337,12 @@ "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.346", + "coreVersion": "13.348", "systemId": "daggerheart", - "systemVersion": "0.0.1", + "systemVersion": "1.1.2", "createdTime": 1754127683751, - "modifiedTime": 1754127795809, - "lastModifiedBy": "MQSznptE5yLT7kj8" + "modifiedTime": 1756511006257, + "lastModifiedBy": "mdk78Q6pOyHh6aBg" }, "_key": "!actors.items!dsfB3YhoL5SudvS2.NnCkXIuATO0s3tSR" }, diff --git a/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json b/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json index af630cdd..6a986acd 100644 --- a/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json +++ b/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json @@ -270,14 +270,14 @@ "name": "Acidic Form", "type": "feature", "system": { - "description": "

When the Ooze makes a successful attack, the target must mark an Armor Slot without receiving its benefi ts (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP.

", + "description": "

When the Ooze makes a successful attack, the target must mark an Armor Slot without receiving its benefits (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP.

", "resource": null, "actions": { "gtT2oHSyZg9OHHJD": { "type": "damage", "_id": "gtT2oHSyZg9OHHJD", "systemPath": "actions", - "description": "

When the Ooze makes a successful attack, the target must mark an Armor Slot without receiving its benefi ts (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP.

", + "description": "

When the Ooze makes a successful attack, the target must mark an Armor Slot without receiving its benefits (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP.

", "chatDisplay": true, "actionType": "action", "cost": [], @@ -343,12 +343,12 @@ "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.346", + "coreVersion": "13.348", "systemId": "daggerheart", - "systemVersion": "0.0.1", + "systemVersion": "1.1.2", "createdTime": 1754129153649, - "modifiedTime": 1754129204931, - "lastModifiedBy": "MQSznptE5yLT7kj8" + "modifiedTime": 1756510982337, + "lastModifiedBy": "mdk78Q6pOyHh6aBg" }, "_key": "!actors.items!6hbqmxDXFOzZJDk4.BQsVuuwFYByKwesR" }, diff --git a/src/packs/adversaries/adversary_Tiny_Green_Ooze_aLkLFuVoKz2NLoBK.json b/src/packs/adversaries/adversary_Tiny_Green_Ooze_aLkLFuVoKz2NLoBK.json index be94d1bd..06227cd5 100644 --- a/src/packs/adversaries/adversary_Tiny_Green_Ooze_aLkLFuVoKz2NLoBK.json +++ b/src/packs/adversaries/adversary_Tiny_Green_Ooze_aLkLFuVoKz2NLoBK.json @@ -224,14 +224,14 @@ "_id": "WpOh5kHHx7lcTvEY", "img": "icons/magic/acid/dissolve-drip-droplet-smoke.webp", "system": { - "description": "

When the Ooze makes a successful attack, the target must mark an Armor Slot without receiving its benefi ts (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP.

", + "description": "

When the Ooze makes a successful attack, the target must mark an Armor Slot without receiving its benefits (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP.

", "resource": null, "actions": { "HfK0u0c7NRppuF1Q": { "type": "damage", "_id": "HfK0u0c7NRppuF1Q", "systemPath": "actions", - "description": "

When the Ooze makes a successful attack, the target must mark an Armor Slot without receiving its benefi ts (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP.

", + "description": "

When the Ooze makes a successful attack, the target must mark an Armor Slot without receiving its benefits (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP.

", "chatDisplay": true, "actionType": "action", "cost": [], @@ -296,12 +296,12 @@ "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.346", + "coreVersion": "13.348", "systemId": "daggerheart", - "systemVersion": "0.0.1", + "systemVersion": "1.1.2", "createdTime": 1754055148507, - "modifiedTime": 1754145130460, - "lastModifiedBy": "MQSznptE5yLT7kj8" + "modifiedTime": 1756510967769, + "lastModifiedBy": "mdk78Q6pOyHh6aBg" }, "_key": "!actors.items!aLkLFuVoKz2NLoBK.WpOh5kHHx7lcTvEY" } From 31238113c994c5c0aa81af25aa8de6333db3ad6b Mon Sep 17 00:00:00 2001 From: Dapoulp <74197441+Dapoulp@users.noreply.github.com> Date: Sat, 30 Aug 2025 17:44:16 +0200 Subject: [PATCH 05/18] Fix/1116 fix action nullable fields (#1117) * Temp ActionField attack type missing * Move missing attack type to getModel * Damage/Healing fields nullable fix * Fix TargetField null value * Other fixes * Fix Action type to be not nullable --- module/data/action/baseAction.mjs | 3 ++- module/data/fields/action/damageField.mjs | 12 ++++++++++-- module/data/fields/action/rollField.mjs | 12 +++++++++--- module/data/fields/action/saveField.mjs | 4 +++- module/data/fields/action/targetField.mjs | 3 ++- 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index 2f5935da..7ea7331a 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -32,7 +32,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel actionType: new fields.StringField({ choices: CONFIG.DH.ITEM.actionTypes, initial: 'action', - nullable: true + nullable: false, + required: true }) }; diff --git a/module/data/fields/action/damageField.mjs b/module/data/fields/action/damageField.mjs index 810b6e76..e870e229 100644 --- a/module/data/fields/action/damageField.mjs +++ b/module/data/fields/action/damageField.mjs @@ -22,10 +22,18 @@ export class DHActionDiceData extends foundry.abstract.DataModel { multiplier: new fields.StringField({ choices: CONFIG.DH.GENERAL.multiplierTypes, initial: 'prof', - label: 'Multiplier' + label: 'Multiplier', + nullable: false, + required: true }), flatMultiplier: new fields.NumberField({ nullable: true, initial: 1, label: 'Flat Multiplier' }), - dice: new fields.StringField({ choices: CONFIG.DH.GENERAL.diceTypes, initial: 'd6', label: 'Dice' }), + dice: new fields.StringField({ + choices: CONFIG.DH.GENERAL.diceTypes, + initial: 'd6', + label: 'Dice', + nullable: false, + required: true + }), bonus: new fields.NumberField({ nullable: true, initial: null, label: 'Bonus' }), custom: new fields.SchemaField({ enabled: new fields.BooleanField({ label: 'Custom Formula' }), diff --git a/module/data/fields/action/rollField.mjs b/module/data/fields/action/rollField.mjs index a4df2a9e..07a113d3 100644 --- a/module/data/fields/action/rollField.mjs +++ b/module/data/fields/action/rollField.mjs @@ -10,13 +10,17 @@ export class DHActionRollData extends foundry.abstract.DataModel { bonus: new fields.NumberField({ nullable: true, initial: null, integer: true }), advState: new fields.StringField({ choices: CONFIG.DH.ACTIONS.advantageState, - initial: 'neutral' + initial: 'neutral', + nullable: false, + required: true }), diceRolling: new fields.SchemaField({ multiplier: new fields.StringField({ choices: CONFIG.DH.GENERAL.diceSetNumbers, initial: 'prof', - label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.multiplier' + label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.multiplier', + nullable: false, + required: true }), flatMultiplier: new fields.NumberField({ nullable: true, @@ -26,7 +30,9 @@ export class DHActionRollData extends foundry.abstract.DataModel { dice: new fields.StringField({ choices: CONFIG.DH.GENERAL.diceTypes, initial: CONFIG.DH.GENERAL.diceTypes.d6, - label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.dice' + label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.dice', + nullable: false, + required: true }), compare: new fields.StringField({ choices: CONFIG.DH.ACTIONS.diceCompare, diff --git a/module/data/fields/action/saveField.mjs b/module/data/fields/action/saveField.mjs index e93a82a9..23378e05 100644 --- a/module/data/fields/action/saveField.mjs +++ b/module/data/fields/action/saveField.mjs @@ -11,7 +11,9 @@ export default class SaveField extends fields.SchemaField { difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }), damageMod: new fields.StringField({ initial: CONFIG.DH.ACTIONS.damageOnSave.none.id, - choices: CONFIG.DH.ACTIONS.damageOnSave + choices: CONFIG.DH.ACTIONS.damageOnSave, + nullable: false, + required: true }) }; super(saveFields, options, context); diff --git a/module/data/fields/action/targetField.mjs b/module/data/fields/action/targetField.mjs index bfb01db9..9dbf1ed9 100644 --- a/module/data/fields/action/targetField.mjs +++ b/module/data/fields/action/targetField.mjs @@ -6,7 +6,8 @@ export default class TargetField extends fields.SchemaField { type: new fields.StringField({ choices: CONFIG.DH.GENERAL.targetTypes, initial: CONFIG.DH.GENERAL.targetTypes.any.id, - nullable: true + nullable: true, + blank: true }), amount: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }) }; From 179dd3e6c32452049d284837aefa9aa92b418f6c Mon Sep 17 00:00:00 2001 From: Chris Ryan <73275196+chrisryan10@users.noreply.github.com> Date: Tue, 2 Sep 2025 10:50:48 +1000 Subject: [PATCH 06/18] Fix the typos related to the Protective feature on Round Shields (#1131) Co-authored-by: Chris Ryan --- ...Advanced_Round_Shield_hiEOGF2reabGLUoi.json | 18 ++++++++++++------ ...Improved_Round_Shield_DlinEBGZfIlvreO3.json | 18 ++++++++++++------ ...egendary_Round_Shield_A28WL9E2lJ3iLZHW.json | 18 ++++++++++++------ .../weapon_Round_Shield_mxwWKDujgsRcZWPT.json | 18 ++++++++++++------ 4 files changed, 48 insertions(+), 24 deletions(-) diff --git a/src/packs/items/weapons/weapon_Advanced_Round_Shield_hiEOGF2reabGLUoi.json b/src/packs/items/weapons/weapon_Advanced_Round_Shield_hiEOGF2reabGLUoi.json index 9240fbaf..efcd890b 100644 --- a/src/packs/items/weapons/weapon_Advanced_Round_Shield_hiEOGF2reabGLUoi.json +++ b/src/packs/items/weapons/weapon_Advanced_Round_Shield_hiEOGF2reabGLUoi.json @@ -105,13 +105,14 @@ "effects": [ { "name": "Protective", - "description": "Add your character's Tier to your Armor Score", + "description": "

Add the item's Tier to your Armor Score

", "img": "icons/skills/melee/shield-block-gray-orange.webp", "changes": [ { "key": "system.armorScore", "mode": 2, - "value": "ITEM.@system.tier" + "value": "ITEM.@system.tier", + "priority": null } ], "_id": "i5HfkF5aKQuUCTEG", @@ -120,7 +121,12 @@ "disabled": false, "duration": { "startTime": null, - "combat": null + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null }, "origin": null, "tint": "#ffffff", @@ -132,12 +138,12 @@ "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.346", + "coreVersion": "13.348", "systemId": "daggerheart", "systemVersion": "0.0.1", "createdTime": 1753794875150, - "modifiedTime": 1753794875150, - "lastModifiedBy": "FecEtPuoQh6MpjQ0" + "modifiedTime": 1756682958806, + "lastModifiedBy": "mdk78Q6pOyHh6aBg" }, "_key": "!items.effects!hiEOGF2reabGLUoi.i5HfkF5aKQuUCTEG" } diff --git a/src/packs/items/weapons/weapon_Improved_Round_Shield_DlinEBGZfIlvreO3.json b/src/packs/items/weapons/weapon_Improved_Round_Shield_DlinEBGZfIlvreO3.json index 49be7813..41a85d9f 100644 --- a/src/packs/items/weapons/weapon_Improved_Round_Shield_DlinEBGZfIlvreO3.json +++ b/src/packs/items/weapons/weapon_Improved_Round_Shield_DlinEBGZfIlvreO3.json @@ -105,13 +105,14 @@ "effects": [ { "name": "Protective", - "description": "Add your character's Tier to your Armor Score", + "description": "

Add the item's Tier to your Armor Score

", "img": "icons/skills/melee/shield-block-gray-orange.webp", "changes": [ { "key": "system.armorScore", "mode": 2, - "value": "ITEM.@system.tier" + "value": "ITEM.@system.tier", + "priority": null } ], "_id": "cXWSV50apzaNQkdA", @@ -120,7 +121,12 @@ "disabled": false, "duration": { "startTime": null, - "combat": null + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null }, "origin": null, "tint": "#ffffff", @@ -132,12 +138,12 @@ "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.346", + "coreVersion": "13.348", "systemId": "daggerheart", "systemVersion": "0.0.1", "createdTime": 1753794098464, - "modifiedTime": 1753794098464, - "lastModifiedBy": "FecEtPuoQh6MpjQ0" + "modifiedTime": 1756682973559, + "lastModifiedBy": "mdk78Q6pOyHh6aBg" }, "_key": "!items.effects!DlinEBGZfIlvreO3.cXWSV50apzaNQkdA" } diff --git a/src/packs/items/weapons/weapon_Legendary_Round_Shield_A28WL9E2lJ3iLZHW.json b/src/packs/items/weapons/weapon_Legendary_Round_Shield_A28WL9E2lJ3iLZHW.json index 421df201..5b768985 100644 --- a/src/packs/items/weapons/weapon_Legendary_Round_Shield_A28WL9E2lJ3iLZHW.json +++ b/src/packs/items/weapons/weapon_Legendary_Round_Shield_A28WL9E2lJ3iLZHW.json @@ -105,13 +105,14 @@ "effects": [ { "name": "Protective", - "description": "Add your character's Tier to your Armor Score", + "description": "

Add the item's Tier to your Armor Score

", "img": "icons/skills/melee/shield-block-gray-orange.webp", "changes": [ { "key": "system.armorScore", "mode": 2, - "value": "ITEM.@system.tier" + "value": "ITEM.@system.tier", + "priority": null } ], "_id": "Z2p00q5h6x6seXys", @@ -120,7 +121,12 @@ "disabled": false, "duration": { "startTime": null, - "combat": null + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null }, "origin": null, "tint": "#ffffff", @@ -132,12 +138,12 @@ "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.346", + "coreVersion": "13.348", "systemId": "daggerheart", "systemVersion": "0.0.1", "createdTime": 1753796983285, - "modifiedTime": 1753796983285, - "lastModifiedBy": "FecEtPuoQh6MpjQ0" + "modifiedTime": 1756682777682, + "lastModifiedBy": "mdk78Q6pOyHh6aBg" }, "_key": "!items.effects!A28WL9E2lJ3iLZHW.Z2p00q5h6x6seXys" } diff --git a/src/packs/items/weapons/weapon_Round_Shield_mxwWKDujgsRcZWPT.json b/src/packs/items/weapons/weapon_Round_Shield_mxwWKDujgsRcZWPT.json index c49aa7dd..5ba5de2b 100644 --- a/src/packs/items/weapons/weapon_Round_Shield_mxwWKDujgsRcZWPT.json +++ b/src/packs/items/weapons/weapon_Round_Shield_mxwWKDujgsRcZWPT.json @@ -105,13 +105,14 @@ "effects": [ { "name": "Protective", - "description": "Add your character's Tier to your Armor Score", + "description": "

Add the item's Tier to your Armor Score.

", "img": "icons/skills/melee/shield-block-gray-orange.webp", "changes": [ { "key": "system.armorScore", "mode": 2, - "value": "ITEM.@system.tier" + "value": "ITEM.@system.tier", + "priority": null } ], "_id": "M70a81e0Mg66jHRL", @@ -120,7 +121,12 @@ "disabled": false, "duration": { "startTime": null, - "combat": null + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null }, "origin": null, "tint": "#ffffff", @@ -132,12 +138,12 @@ "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.346", + "coreVersion": "13.348", "systemId": "daggerheart", "systemVersion": "0.0.1", "createdTime": 1753794114980, - "modifiedTime": 1753794114980, - "lastModifiedBy": "FecEtPuoQh6MpjQ0" + "modifiedTime": 1756682994216, + "lastModifiedBy": "mdk78Q6pOyHh6aBg" }, "_key": "!items.effects!mxwWKDujgsRcZWPT.M70a81e0Mg66jHRL" } From 1b9defe4ad960afef9d38695df8de1d0ad8c2a1e Mon Sep 17 00:00:00 2001 From: Chris Ryan <73275196+chrisryan10@users.noreply.github.com> Date: Tue, 2 Sep 2025 23:00:46 +1000 Subject: [PATCH 07/18] The uuid is expected, not id (#1132) Co-authored-by: Chris Ryan --- module/applications/sheets/api/application-mixin.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index bdcee27a..fc158ace 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -401,7 +401,7 @@ export default function DHApplicationMixin(Base) { options.push({ name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat', icon: 'fa-solid fa-message', - callback: async target => (await getDocFromElement(target)).toChat(this.document.id) + callback: async target => (await getDocFromElement(target)).toChat(this.document.uuid) }); if (deletable) From 3c893df175e463538a5d6bc079fede4ef7ea8d61 Mon Sep 17 00:00:00 2001 From: Chris Ryan <73275196+chrisryan10@users.noreply.github.com> Date: Tue, 2 Sep 2025 23:26:32 +1000 Subject: [PATCH 08/18] Fix the missing translation keys (#1133) Co-authored-by: Chris Ryan --- module/config/generalConfig.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 34ca6009..60b3a2f1 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -574,17 +574,17 @@ export const abilityCosts = { }, hope: { id: 'hope', - label: 'Hope', + label: 'DAGGERHEART.CONFIG.HealingType.hope.name', group: 'TYPES.Actor.character' }, armor: { id: 'armor', - label: 'Armor Slot', + label: 'DAGGERHEART.CONFIG.HealingType.armor.name', group: 'TYPES.Actor.character' }, fear: { id: 'fear', - label: 'Fear', + label: 'DAGGERHEART.CONFIG.HealingType.fear.name', group: 'TYPES.Actor.adversary' } }; From f04619f73b169b03df0fd1e6b27721512f9bfe13 Mon Sep 17 00:00:00 2001 From: Dapoulp <74197441+Dapoulp@users.noreply.github.com> Date: Fri, 5 Sep 2025 02:45:05 +0200 Subject: [PATCH 09/18] Fix/1144 fix experiences roll use costs (#1150) * Temp ActionField attack type missing * Move missing attack type to getModel * Fix costs object being updated during getRealCosts --- module/data/fields/action/costField.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/module/data/fields/action/costField.mjs b/module/data/fields/action/costField.mjs index f4d942b1..4c8fad88 100644 --- a/module/data/fields/action/costField.mjs +++ b/module/data/fields/action/costField.mjs @@ -94,7 +94,8 @@ export default class CostField extends fields.ArrayField { } static getRealCosts(costs) { - const realCosts = costs?.length ? costs.filter(c => c.enabled) : []; + const cloneCosts = foundry.utils.deepClone(costs), + realCosts = cloneCosts?.length ? cloneCosts.filter(c => c.enabled) : []; let mergedCosts = []; realCosts.forEach(c => { const getCost = Object.values(mergedCosts).find(gc => gc.key === c.key); From 2d92576121115f55ec42953890ad97db5130d66e Mon Sep 17 00:00:00 2001 From: Dapoulp <74197441+Dapoulp@users.noreply.github.com> Date: Fri, 5 Sep 2025 02:56:38 +0200 Subject: [PATCH 10/18] Fix/1149 fix targets apply effects (#1151) * Temp ActionField attack type missing * Move missing attack type to getModel * Fix targets apply effects --- module/documents/chatMessage.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index b8667384..3567cb90 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -169,7 +169,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { if (this.system.source.item && this.system.source.action) { const action = this.getAction(actor, this.system.source.item, this.system.source.action); if (!action || !action?.applyEffects) return; - const targets = this.getTargetList(); + const targets = this.system.hitTargets; if (targets.length === 0) ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected')); this.consumeOnSuccess(); From ec54da4e23847b4dfea82a3ab71ad9cacddba2c0 Mon Sep 17 00:00:00 2001 From: Chris Ryan <73275196+chrisryan10@users.noreply.github.com> Date: Fri, 5 Sep 2025 23:47:12 +1000 Subject: [PATCH 11/18] Add and fix Tier names in level up (#1142) Co-authored-by: Chris Ryan --- lang/en.json | 3 +++ module/data/levelTier.mjs | 6 +++--- module/data/levelup.mjs | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lang/en.json b/lang/en.json index 7efca151..f1164232 100755 --- a/lang/en.json +++ b/lang/en.json @@ -475,18 +475,21 @@ }, "takeLevelUp": "Finish Level Up", "tier2": { + "name": "Tier 2", "label": "Levels 2-4", "infoLabel": "At Level 2, gain an additional Experience at +2 and gain a +1 bonus to your Proficiency.", "pretext": "Choose two options from the list below", "posttext": "Take an additional domain card of your level or lower from a domain you have access to." }, "tier3": { + "name": "Tier 3", "label": "Levels 5-7", "infoLabel": "At Level 5, take an additional Experience and clear all marks on Character Traits.", "pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.", "posttext": "Take an additional domain card of your level or lower from a domain you have access to." }, "tier4": { + "name": "Tier 4", "label": "Levels 8-10", "infoLabel": "At Level 8, take an additional Experience and clear all marks on Character Traits.", "pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.", diff --git a/module/data/levelTier.mjs b/module/data/levelTier.mjs index e42cfc54..2252e4da 100644 --- a/module/data/levelTier.mjs +++ b/module/data/levelTier.mjs @@ -169,7 +169,7 @@ export const defaultLevelTiers = { tiers: { 2: { tier: 2, - name: 'Tier 2', + name: 'DAGGERHEART.APPLICATIONS.Levelup.tier2.name', levels: { start: 2, end: 4 @@ -232,7 +232,7 @@ export const defaultLevelTiers = { }, 3: { tier: 3, - name: 'Tier 3', + name: 'DAGGERHEART.APPLICATIONS.Levelup.tier3.name', levels: { start: 5, end: 7 @@ -313,7 +313,7 @@ export const defaultLevelTiers = { }, 4: { tier: 4, - name: 'Tier 4', + name: 'DAGGERHEART.APPLICATIONS.Levelup.tier4.name', levels: { start: 8, end: 10 diff --git a/module/data/levelup.mjs b/module/data/levelup.mjs index 665b3264..4dc1c058 100644 --- a/module/data/levelup.mjs +++ b/module/data/levelup.mjs @@ -234,7 +234,7 @@ export class DhLevelup extends foundry.abstract.DataModel { const subclassInTier = subclasses.some(x => x.tier === Number(tierKey)); return { - name: tier.name, + name: game.i18n.localize(tier.name), active: this.currentLevel >= Math.min(...tier.belongingLevels), groups: Object.keys(tier.options).map(optionKey => { const option = tier.options[optionKey]; From d4a98d3f662e0668057fee13e1d827d1812d2645 Mon Sep 17 00:00:00 2001 From: Chris Ryan <73275196+chrisryan10@users.noreply.github.com> Date: Sun, 7 Sep 2025 03:53:21 +1000 Subject: [PATCH 12/18] Add motives and tactics for Oak Treant (#1158) Co-authored-by: Chris Ryan --- .../adversary_Oak_Treant_XK78QUfY8c8Go8Uv.json | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/packs/adversaries/adversary_Oak_Treant_XK78QUfY8c8Go8Uv.json b/src/packs/adversaries/adversary_Oak_Treant_XK78QUfY8c8Go8Uv.json index f32a9894..22cbc3d3 100644 --- a/src/packs/adversaries/adversary_Oak_Treant_XK78QUfY8c8Go8Uv.json +++ b/src/packs/adversaries/adversary_Oak_Treant_XK78QUfY8c8Go8Uv.json @@ -105,19 +105,20 @@ "img": "icons/skills/melee/blood-slash-foam-red.webp", "type": "attack", "chatDisplay": false - } + }, + "motivesAndTactics": "Hide in plain sight, preserve the forest, root down, swing branches" }, "flags": {}, "_stats": { "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.347", + "coreVersion": "13.348", "systemId": "daggerheart", - "systemVersion": "0.0.1", + "systemVersion": "1.1.2", "createdTime": 1753922784314, - "modifiedTime": 1755259462770, - "lastModifiedBy": "VZIeX2YDvX338Zvr" + "modifiedTime": 1757057641714, + "lastModifiedBy": "mdk78Q6pOyHh6aBg" }, "_id": "XK78QUfY8c8Go8Uv", "sort": 3400000, From e258d9c5f66d0e42ea820ec7654ef13f053269ee Mon Sep 17 00:00:00 2001 From: Chris Ryan <73275196+chrisryan10@users.noreply.github.com> Date: Sun, 7 Sep 2025 06:05:24 +1000 Subject: [PATCH 13/18] Fixed typo in button class (#1159) Co-authored-by: Chris Ryan --- templates/dialogs/dice-roll/rollSelection.hbs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/dialogs/dice-roll/rollSelection.hbs b/templates/dialogs/dice-roll/rollSelection.hbs index 3aac0321..a2ca5a01 100644 --- a/templates/dialogs/dice-roll/rollSelection.hbs +++ b/templates/dialogs/dice-roll/rollSelection.hbs @@ -148,7 +148,7 @@ - From fd925407925e4d91c4f96bfb7689cf0d043af9a8 Mon Sep 17 00:00:00 2001 From: Chris Ryan <73275196+chrisryan10@users.noreply.github.com> Date: Sun, 7 Sep 2025 06:06:17 +1000 Subject: [PATCH 14/18] Add keys, update usage for translations (#1156) Co-authored-by: Chris Ryan --- lang/en.json | 12 +++++++++++- module/applications/sheets-configs/action-config.mjs | 6 +++--- module/config/generalConfig.mjs | 8 ++++---- templates/actionTypes/cost.hbs | 10 +++++----- templates/actionTypes/range-target.hbs | 6 +++--- templates/actionTypes/uses.hbs | 6 +++--- 6 files changed, 29 insertions(+), 19 deletions(-) diff --git a/lang/en.json b/lang/en.json index f1164232..37e6cfff 100755 --- a/lang/en.json +++ b/lang/en.json @@ -992,6 +992,12 @@ "selectType": "Select Action Type", "selectAction": "Action Selection" }, + "TargetTypes": { + "any": "Any", + "friendly": "Friendly", + "hostile": "Hostile", + "self": "Self" + }, "TemplateTypes": { "circle": "Circle", "cone": "Cone", @@ -1880,7 +1886,9 @@ "tier4": "tier 4", "domains": "Domains", "downtime": "Downtime", - "rules": "Rules" + "rules": "Rules", + "configuration": "Configuration", + "base": "Base" }, "Tiers": { "singular": "Tier", @@ -1981,6 +1989,8 @@ "save": "Save", "scalable": "Scalable", "situationalBonus": "Situational Bonus", + "spent": "Spent", + "step": "Step", "stress": "Stress", "subclasses": "Subclasses", "success": "Success", diff --git a/module/applications/sheets-configs/action-config.mjs b/module/applications/sheets-configs/action-config.mjs index ea3c6f31..f673fc66 100644 --- a/module/applications/sheets-configs/action-config.mjs +++ b/module/applications/sheets-configs/action-config.mjs @@ -66,7 +66,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { group: 'primary', id: 'base', icon: null, - label: 'Base' + label: 'DAGGERHEART.GENERAL.Tabs.base' }, config: { active: false, @@ -74,7 +74,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { group: 'primary', id: 'config', icon: null, - label: 'Configuration' + label: 'DAGGERHEART.GENERAL.Tabs.configuration' }, effect: { active: false, @@ -82,7 +82,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { group: 'primary', id: 'effect', icon: null, - label: 'Effect' + label: 'DAGGERHEART.GENERAL.Tabs.effects' } }; diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 60b3a2f1..8714d44b 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -90,22 +90,22 @@ export const rangeInclusion = { export const otherTargetTypes = { friendly: { id: 'friendly', - label: 'Friendly' + label: 'DAGGERHEART.CONFIG.TargetTypes.friendly' }, hostile: { id: 'hostile', - label: 'Hostile' + label: 'DAGGERHEART.CONFIG.TargetTypes.hostile' }, any: { id: 'any', - label: 'Any' + label: 'DAGGERHEART.CONFIG.TargetTypes.any' } }; export const targetTypes = { self: { id: 'self', - label: 'Self' + label: 'DAGGERHEART.CONFIG.TargetTypes.self' }, ...otherTargetTypes }; diff --git a/templates/actionTypes/cost.hbs b/templates/actionTypes/cost.hbs index a1b7de48..7a9f33d9 100644 --- a/templates/actionTypes/cost.hbs +++ b/templates/actionTypes/cost.hbs @@ -1,6 +1,6 @@
- Cost + {{localize "DAGGERHEART.GENERAL.Cost.single"}} {{#each source as |cost index|}} @@ -8,10 +8,10 @@ {{formField ../fields.consumeOnSuccess value=cost.consumeOnSuccess name=(concat "cost." index ".consumeOnSuccess") classes="checkbox" rootId=partId localize=true}} {{/if}}
- {{formField ../fields.scalable label="Scalable" value=cost.scalable name=(concat "cost." index ".scalable") classes="checkbox"}} - {{formField ../fields.key choices=(@root.disableOption index @root.costOptions ../source) label="Resource" value=cost.key name=(concat "cost." index ".key") localize=true blank=false}} - {{formField ../fields.value label="Amount" value=cost.value name=(concat "cost." index ".value")}} - {{formField ../fields.step label="Step" value=cost.step name=(concat "cost." index ".step") disabled=(not cost.scalable)}} + {{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.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}}
{{/each}} diff --git a/templates/actionTypes/range-target.hbs b/templates/actionTypes/range-target.hbs index 3776f0c1..143acdf8 100644 --- a/templates/actionTypes/range-target.hbs +++ b/templates/actionTypes/range-target.hbs @@ -1,12 +1,12 @@
{{localize "DAGGERHEART.GENERAL.range"}}{{#if fields.target}} & {{localize "DAGGERHEART.GENERAL.Target.single"}}{{/if}} - {{formField fields.range value=source.range label="Range" name=(concat path "range") localize=true}} + {{formField fields.range value=source.range label="DAGGERHEART.GENERAL.range" name=(concat path "range") localize=true}} {{#if fields.target}}
{{#if (and source.target.type (not (eq source.target.type 'self')))}} - {{ formField fields.target.amount value=source.target.amount label="Amount" name=(concat path "target.amount") }} + {{ formField fields.target.amount value=source.target.amount label="DAGGERHEART.GENERAL.amount" name=(concat path "target.amount") localize=true}} {{/if}} - {{ formField fields.target.type value=source.target.type label="Target" name=(concat path "target.type") localize=true }} + {{ formField fields.target.type value=source.target.type label="DAGGERHEART.GENERAL.Target.single" name=(concat path "target.type") localize=true }}
{{/if}}
\ No newline at end of file diff --git a/templates/actionTypes/uses.hbs b/templates/actionTypes/uses.hbs index 2c1c3cd4..7304f810 100644 --- a/templates/actionTypes/uses.hbs +++ b/templates/actionTypes/uses.hbs @@ -4,8 +4,8 @@ {{formField fields.consumeOnSuccess value=source.consumeOnSuccess name="uses.consumeOnSuccess" classes="checkbox" rootId=partId localize=true}} {{/if}}
- {{formField fields.value label="Spent" value=source.value name="uses.value" rootId=partId}} - {{formField fields.max label="Max" value=source.max name="uses.max" rootId=partId}} + {{formField fields.value label="DAGGERHEART.GENERAL.spent" value=source.value name="uses.value" rootId=partId localize=true}} + {{formField fields.max label="DAGGERHEART.GENERAL.max" value=source.max name="uses.max" rootId=partId localize=true}}
- {{formField fields.recovery label="Recovery" value=source.recovery name="uses.recovery" rootId=partId localize=true}} + {{formField fields.recovery label="DAGGERHEART.GENERAL.recovery" value=source.recovery name="uses.recovery" rootId=partId localize=true}}
\ No newline at end of file From f1b6d3851dbed1559d88ecebc71d1bb537fd8b8b Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sun, 7 Sep 2025 00:30:29 +0200 Subject: [PATCH 15/18] [PR] [Feature] 590 - Daggerheart Menu (#1007) * Added menu with refresh tools * Replaced menu icon --- CONTRIBUTING.md | 4 +- daggerheart.mjs | 8 +- lang/en.json | 12 ++ module/applications/_module.mjs | 1 + module/applications/levelup/levelup.mjs | 2 +- module/applications/scene/_module.mjs | 2 +- .../scene/sceneConfigSettings.mjs | 37 ++-- .../settings/appearanceSettings.mjs | 90 +++++----- .../sheets/api/application-mixin.mjs | 11 +- module/applications/sidebar/_module.mjs | 2 + module/applications/sidebar/sidebar.mjs | 33 ++++ .../sidebar/tabs/daggerheartMenu.mjs | 160 ++++++++++++++++++ module/applications/ui/chatLog.mjs | 2 +- module/applications/ui/fearTracker.mjs | 8 +- module/applications/ui/itemBrowser.mjs | 97 +++++------ module/config/itemBrowserConfig.mjs | 38 +++-- module/config/settingsConfig.mjs | 20 +-- module/data/action/baseAction.mjs | 45 +++-- module/data/chat-message/_modules.mjs | 12 +- module/data/chat-message/actorRoll.mjs | 6 +- module/data/fields/action/beastformField.mjs | 14 +- module/data/fields/action/damageField.mjs | 53 +++--- module/data/fields/action/effectsField.mjs | 25 ++- module/data/fields/action/macroField.mjs | 2 +- module/data/fields/action/rangeField.mjs | 3 +- module/data/fields/action/rollField.mjs | 28 ++- module/data/fields/action/saveField.mjs | 102 +++++------ module/data/fields/action/targetField.mjs | 13 +- module/data/fields/action/usesField.mjs | 6 +- module/data/settings/Appearance.mjs | 27 +-- module/data/settings/Automation.mjs | 8 +- module/dice/damageRoll.mjs | 3 +- module/dice/dhRoll.mjs | 9 +- module/dice/dualityRoll.mjs | 7 +- module/documents/chatMessage.mjs | 34 ++-- module/documents/item.mjs | 2 +- module/helpers/handlebarsHelper.mjs | 4 +- module/systemRegistration/handlebars.mjs | 5 +- module/systemRegistration/socket.mjs | 3 +- pull_request_template.md | 3 +- styles/less/global/elements.less | 4 +- styles/less/global/global.less | 12 +- styles/less/global/sheet.less | 2 +- .../environment/potentialAdversaries.less | 2 +- styles/less/ui/chat/refresh-message.less | 13 ++ styles/less/ui/index.less | 5 +- styles/less/ui/item-browser/item-browser.less | 5 +- styles/less/ui/settings/settings.less | 4 +- styles/less/ui/sidebar/daggerheartMenu.less | 38 +++++ styles/less/ui/sidebar/tabs.less | 8 + templates/sidebar/daggerheart-menu/main.hbs | 22 +++ templates/sidebar/tabs.hbs | 18 ++ templates/ui/chat/refreshMessage.hbs | 6 + 53 files changed, 730 insertions(+), 350 deletions(-) create mode 100644 module/applications/sidebar/_module.mjs create mode 100644 module/applications/sidebar/sidebar.mjs create mode 100644 module/applications/sidebar/tabs/daggerheartMenu.mjs create mode 100644 styles/less/ui/chat/refresh-message.less create mode 100644 styles/less/ui/sidebar/daggerheartMenu.less create mode 100644 styles/less/ui/sidebar/tabs.less create mode 100644 templates/sidebar/daggerheart-menu/main.hbs create mode 100644 templates/sidebar/tabs.hbs create mode 100644 templates/ui/chat/refreshMessage.hbs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 80c01bce..b9099005 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,12 +44,14 @@ We encourage contributors to leave comments or open Discussions when proposing s ## 🧾 Issue & PR Best Practices **For Issues:** + - Use clear, descriptive titles - Provide a concise explanation of the problem or idea - Include reproduction steps or example scenarios if it's a bug - Add screenshots or logs if helpful **For Pull Requests:** + - Use a clear title summarizing the change - Provide a brief description of what your code does and why - Link to any related Issues @@ -71,6 +73,6 @@ Discussions are currently happening on GitHub — in Issues, PRs, and [GitHub Di ## 🤗 Thank You! -Whether you're fixing a typo or designing entire mechanics — every contribution matters. Thank you for helping bring *Daggerheart* to life in FoundryVTT through **Foundryborne**! +Whether you're fixing a typo or designing entire mechanics — every contribution matters. Thank you for helping bring _Daggerheart_ to life in FoundryVTT through **Foundryborne**! 🐸🛠️ diff --git a/daggerheart.mjs b/daggerheart.mjs index 1c4c2a85..f5f5e303 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -133,6 +133,8 @@ Hooks.once('init', () => { CONFIG.ui.combat = applications.ui.DhCombatTracker; CONFIG.ui.chat = applications.ui.DhChatLog; CONFIG.ui.hotbar = applications.ui.DhHotbar; + CONFIG.ui.sidebar = applications.sidebar.DhSidebar; + CONFIG.ui.daggerheartMenu = applications.sidebar.DaggerheartMenu; CONFIG.Token.rulerClass = placeables.DhTokenRuler; CONFIG.ui.resources = applications.ui.DhFearTracker; @@ -162,7 +164,7 @@ Hooks.on('ready', async () => { if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).displayFear !== 'hide') ui.resources.render({ force: true }); - if(!(ui.compendiumBrowser instanceof applications.ui.ItemBrowser)) + if (!(ui.compendiumBrowser instanceof applications.ui.ItemBrowser)) ui.compendiumBrowser = new applications.ui.ItemBrowser(); registerCountdownHooks(); @@ -309,5 +311,5 @@ Hooks.on('moveToken', async (movedToken, data) => { } }); -Hooks.on("renderCompendiumDirectory", (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html)); -Hooks.on("renderDocumentDirectory", (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html)); +Hooks.on('renderCompendiumDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html)); +Hooks.on('renderDocumentDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html)); diff --git a/lang/en.json b/lang/en.json index d5c4f037..8de962bd 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2420,6 +2420,10 @@ "heal": "Heal", "applyHealing": "Apply Healing" }, + "refreshMessage": { + "title": "Feature Refresh", + "header": "Refreshed" + }, "reroll": { "confirmTitle": "Reroll Dice", "confirmText": "Are you sure you want to reroll?" @@ -2534,8 +2538,16 @@ "multiclassAlreadyPresent": "You already have a class and multiclass", "subclassesAlreadyPresent": "You already have a class and multiclass subclass", "noDiceSystem": "Your selected dice {system} does not have a {faces} dice", + "gmMenuRefresh": "You refreshed all actions and resources {types}", "subclassAlreadyLinked": "{name} is already a subclass in the class {class}. Remove it from there if you want it to be a subclass to this class." }, + "Sidebar": { + "daggerheartMenu": { + "title": "Daggerheart Menu", + "startSession": "Start Session", + "startScene": "Start Scene" + } + }, "Tooltip": { "disableEffect": "Disable Effect", "enableEffect": "Enable Effect", diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs index 3dd0c78f..b2d3001a 100644 --- a/module/applications/_module.mjs +++ b/module/applications/_module.mjs @@ -6,5 +6,6 @@ export * as scene from './scene/_module.mjs'; export * as settings from './settings/_module.mjs'; export * as sheets from './sheets/_module.mjs'; export * as sheetConfigs from './sheets-configs/_module.mjs'; +export * as sidebar from './sidebar/_module.mjs'; export * as ui from './ui/_module.mjs'; export * as ux from './ux/_module.mjs'; diff --git a/module/applications/levelup/levelup.mjs b/module/applications/levelup/levelup.mjs index 99cc53f6..c3cc6e81 100644 --- a/module/applications/levelup/levelup.mjs +++ b/module/applications/levelup/levelup.mjs @@ -536,7 +536,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) static async viewCompendium(event, target) { const type = target.dataset.compendium ?? target.dataset.type; - const presets = { + const presets = { folder: type, render: { noFolder: true diff --git a/module/applications/scene/_module.mjs b/module/applications/scene/_module.mjs index 6dc59081..7cefd268 100644 --- a/module/applications/scene/_module.mjs +++ b/module/applications/scene/_module.mjs @@ -1 +1 @@ -export { default as DhSceneConfigSettings } from './sceneConfigSettings.mjs'; \ No newline at end of file +export { default as DhSceneConfigSettings } from './sceneConfigSettings.mjs'; diff --git a/module/applications/scene/sceneConfigSettings.mjs b/module/applications/scene/sceneConfigSettings.mjs index 2ad7421a..40f66ae2 100644 --- a/module/applications/scene/sceneConfigSettings.mjs +++ b/module/applications/scene/sceneConfigSettings.mjs @@ -1,25 +1,24 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.SceneConfig { - constructor(options, ...args) { - super(options, ...args); - } - - static buildParts() { - const { footer, ...parts } = super.PARTS; - const tmpParts = { - ...parts, - dh: { template: "systems/daggerheart/templates/scene/dh-config.hbs" }, - footer + constructor(options, ...args) { + super(options, ...args); } - return tmpParts; - } - static PARTS = DhSceneConfigSettings.buildParts(); + static buildParts() { + const { footer, ...parts } = super.PARTS; + const tmpParts = { + ...parts, + dh: { template: 'systems/daggerheart/templates/scene/dh-config.hbs' }, + footer + }; + return tmpParts; + } - static buildTabs() { - super.TABS.sheet.tabs.push({ id: "dh", icon: "fa-solid" }); - return super.TABS; - } + static PARTS = DhSceneConfigSettings.buildParts(); - static TABS = DhSceneConfigSettings.buildTabs(); + static buildTabs() { + super.TABS.sheet.tabs.push({ id: 'dh', icon: 'fa-solid' }); + return super.TABS; + } -} \ No newline at end of file + static TABS = DhSceneConfigSettings.buildTabs(); +} diff --git a/module/applications/settings/appearanceSettings.mjs b/module/applications/settings/appearanceSettings.mjs index bbc27a79..5950f961 100644 --- a/module/applications/settings/appearanceSettings.mjs +++ b/module/applications/settings/appearanceSettings.mjs @@ -33,7 +33,7 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, main: { template: 'systems/daggerheart/templates/settings/appearance-settings/main.hbs' }, diceSoNice: { template: 'systems/daggerheart/templates/settings/appearance-settings/diceSoNice.hbs' }, - footer: { template: "templates/generic/form-footer.hbs" } + footer: { template: 'templates/generic/form-footer.hbs' } }; /** @inheritdoc */ @@ -41,7 +41,7 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App general: { tabs: [ { id: 'main', label: 'DAGGERHEART.GENERAL.Tabs.general' }, - { id: 'diceSoNice', label: 'DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.title' }, + { id: 'diceSoNice', label: 'DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.title' } ], initial: 'main' }, @@ -73,7 +73,7 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App /** @inheritdoc */ _configureRenderParts(options) { const parts = super._configureRenderParts(options); - if (!game.modules.get('dice-so-nice')?.active){ + if (!game.modules.get('dice-so-nice')?.active) { delete parts.diceSoNice; delete parts.tabs; } @@ -83,7 +83,8 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App /**@inheritdoc */ async _prepareContext(options) { const context = await super._prepareContext(options); - if (options.isFirstRender) this.setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance); + if (options.isFirstRender) + this.setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance); context.setting = this.setting; context.fields = this.setting.schema.fields; @@ -99,18 +100,17 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App const partContext = await super._preparePartContext(partId, context, options); if (partId in context.tabs) partContext.tab = partContext.tabs[partId]; switch (partId) { - case "diceSoNice": + case 'diceSoNice': await this.prepareDiceSoNiceContext(partContext); break; - case "footer": + case 'footer': partContext.buttons = [ - { type: "button", action: "reset", icon: "fa-solid fa-arrow-rotate-left", label: "Reset" }, - { type: "submit", icon: "fa-solid fa-floppy-disk", label: "Save Changes" } + { type: 'button', action: 'reset', icon: 'fa-solid fa-arrow-rotate-left', label: 'Reset' }, + { type: 'submit', icon: 'fa-solid fa-floppy-disk', label: 'Save Changes' } ]; break; } return partContext; - } /** @@ -120,32 +120,44 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App * @protected */ async prepareDiceSoNiceContext(context) { - context.diceSoNiceTextures = Object.entries(game.dice3d.exports.TEXTURELIST).reduce((acc, [k, v]) => ({ - ...acc, - [k]: v.name - }), {}); - context.diceSoNiceColorsets = Object.values(game.dice3d.exports.COLORSETS).reduce((acc, v) => ({ - ...acc, - [v.id]: v.description - }), {}); - context.diceSoNiceMaterials = Object.keys(game.dice3d.DiceFactory.material_options).reduce((acc, key) => ({ - ...acc, - [key]: `DICESONICE.Material${key.capitalize()}` - }), {}); - context.diceSoNiceSystems = Object.fromEntries([...game.dice3d.DiceFactory.systems].map(([k, v]) => [k, v.name])); + context.diceSoNiceTextures = Object.entries(game.dice3d.exports.TEXTURELIST).reduce( + (acc, [k, v]) => ({ + ...acc, + [k]: v.name + }), + {} + ); + context.diceSoNiceColorsets = Object.values(game.dice3d.exports.COLORSETS).reduce( + (acc, v) => ({ + ...acc, + [v.id]: v.description + }), + {} + ); + context.diceSoNiceMaterials = Object.keys(game.dice3d.DiceFactory.material_options).reduce( + (acc, key) => ({ + ...acc, + [key]: `DICESONICE.Material${key.capitalize()}` + }), + {} + ); + context.diceSoNiceSystems = Object.fromEntries( + [...game.dice3d.DiceFactory.systems].map(([k, v]) => [k, v.name]) + ); - foundry.utils.mergeObject(context.dsnTabs, [ - "hope", - "fear", - "advantage", - "disadvantage", - ].reduce((acc, key) => ({ - ...acc, - [key]: { - values: this.setting.diceSoNice[key], - fields: this.setting.schema.getField(`diceSoNice.${key}`).fields, - } - }), {})); + foundry.utils.mergeObject( + context.dsnTabs, + ['hope', 'fear', 'advantage', 'disadvantage'].reduce( + (acc, key) => ({ + ...acc, + [key]: { + values: this.setting.diceSoNice[key], + fields: this.setting.schema.getField(`diceSoNice.${key}`).fields + } + }), + {} + ) + ); } /** @@ -169,7 +181,7 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App * @type {ApplicationClickAction} */ static async #onPreview(_, target) { - const formData = new foundry.applications.ux.FormDataExtended(target.closest("form")); + const formData = new foundry.applications.ux.FormDataExtended(target.closest('form')); const { diceSoNice } = foundry.utils.expandObject(formData.object); const { key } = target.dataset; const faces = ['advantage', 'disadvantage'].includes(key) ? 'd6' : 'd12'; @@ -181,10 +193,10 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App } /** - * Reset the form back to default values. - * @this {DHAppearanceSettings} - * @type {ApplicationClickAction} - */ + * Reset the form back to default values. + * @this {DHAppearanceSettings} + * @type {ApplicationClickAction} + */ static async #onReset() { this.setting = new this.setting.constructor(); this.render({ force: false }); diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 2158e48b..1a508131 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -416,11 +416,14 @@ export default function DHApplicationMixin(Base) { icon: 'fa-solid fa-ban', condition: target => { const doc = getDocFromElementSync(target); - return doc && doc.system?.actions?.some(a => a.type === "beastform"); + return doc && doc.system?.actions?.some(a => a.type === 'beastform'); }, - callback: async target => game.system.api.fields.ActionFields.BeastformField.handleActiveTransformations.call(await getDocFromElement(target)) + callback: async target => + game.system.api.fields.ActionFields.BeastformField.handleActiveTransformations.call( + await getDocFromElement(target) + ) }); - + options.unshift({ name: 'DAGGERHEART.GENERAL.damage', icon: 'fa-solid fa-explosion', @@ -433,7 +436,7 @@ export default function DHApplicationMixin(Base) { action = doc?.system?.attack ?? doc; const config = action.prepareConfig(event); config.hasRoll = false; - return action && action.workflow.get("damage").execute(config, null, true); + return action && action.workflow.get('damage').execute(config, null, true); } }); diff --git a/module/applications/sidebar/_module.mjs b/module/applications/sidebar/_module.mjs new file mode 100644 index 00000000..f19f697c --- /dev/null +++ b/module/applications/sidebar/_module.mjs @@ -0,0 +1,2 @@ +export { default as DaggerheartMenu } from './tabs/daggerheartMenu.mjs'; +export { default as DhSidebar } from './sidebar.mjs'; diff --git a/module/applications/sidebar/sidebar.mjs b/module/applications/sidebar/sidebar.mjs new file mode 100644 index 00000000..fad39ac5 --- /dev/null +++ b/module/applications/sidebar/sidebar.mjs @@ -0,0 +1,33 @@ +export default class DhSidebar extends Sidebar { + /** @override */ + static TABS = { + ...super.TABS, + daggerheartMenu: { + tooltip: 'DAGGERHEART.UI.Sidebar.daggerheartMenu.title', + img: 'systems/daggerheart/assets/logos/FoundryBorneLogoWhite.svg' + } + }; + + /** @override */ + static PARTS = { + tabs: { + id: 'tabs', + template: 'systems/daggerheart/templates/sidebar/tabs.hbs' + } + }; + + /** @override */ + async _prepareTabContext(context, options) { + context.tabs = Object.entries(this.constructor.TABS).reduce((obj, [k, v]) => { + let { documentName, gmOnly, tooltip, icon, img } = v; + if (gmOnly && !game.user.isGM) return obj; + if (documentName) { + tooltip ??= getDocumentClass(documentName).metadata.labelPlural; + icon ??= CONFIG[documentName]?.sidebarIcon; + } + obj[k] = { tooltip, icon, img }; + obj[k].active = this.tabGroups.primary === k; + return obj; + }, {}); + } +} diff --git a/module/applications/sidebar/tabs/daggerheartMenu.mjs b/module/applications/sidebar/tabs/daggerheartMenu.mjs new file mode 100644 index 00000000..cf7aeae3 --- /dev/null +++ b/module/applications/sidebar/tabs/daggerheartMenu.mjs @@ -0,0 +1,160 @@ +const { HandlebarsApplicationMixin } = foundry.applications.api; +const { AbstractSidebarTab } = foundry.applications.sidebar; +/** + * The daggerheart menu tab. + * @extends {AbstractSidebarTab} + * @mixes HandlebarsApplication + */ +export default class DaggerheartMenu extends HandlebarsApplicationMixin(AbstractSidebarTab) { + constructor(options) { + super(options); + + this.refreshSelections = DaggerheartMenu.#defaultRefreshSelections(); + } + + static #defaultRefreshSelections() { + return { + session: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.session') }, + scene: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.scene') }, + longRest: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.longrest') }, + shortRest: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.shortrest') } + }; + } + + /** @override */ + static DEFAULT_OPTIONS = { + classes: ['dh-style'], + window: { + title: 'SIDEBAR.TabSettings' + }, + actions: { + selectRefreshable: DaggerheartMenu.#selectRefreshable, + refreshActors: DaggerheartMenu.#refreshActors + } + }; + + /** @override */ + static tabName = 'daggerheartMenu'; + + /** @override */ + static PARTS = { + main: { template: 'systems/daggerheart/templates/sidebar/daggerheart-menu/main.hbs' } + }; + + /* -------------------------------------------- */ + + /** @inheritDoc */ + async _prepareContext(options) { + const context = await super._prepareContext(options); + context.refreshables = this.refreshSelections; + context.disableRefresh = Object.values(this.refreshSelections).every(x => !x.selected); + + return context; + } + + async getRefreshables(types) { + const refreshedActors = {}; + for (let actor of game.actors) { + if (['character', 'adversary'].includes(actor.type) && actor.prototypeToken.actorLink) { + const updates = {}; + for (let item of actor.items) { + if (item.system.metadata.hasResource && types.includes(item.system.resource?.recovery)) { + if (!refreshedActors[actor.id]) + refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() }; + refreshedActors[actor.id].refreshed.add( + game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[item.system.resource.recovery].label) + ); + + if (!updates[item.id]?.system) updates[item.id] = { system: {} }; + + const increasing = + item.system.resource.progression === CONFIG.DH.ITEM.itemResourceProgression.increasing.id; + updates[item.id].system = { + ...updates[item.id].system, + 'resource.value': increasing + ? 0 + : Roll.replaceFormulaData(item.system.resource.max, actor.getRollData()) + }; + } + if (item.system.metadata.hasActions) { + const refreshTypes = new Set(); + const actions = item.system.actions.filter(action => { + if (types.includes(action.uses.recovery)) { + refreshTypes.add(action.uses.recovery); + return true; + } + + return false; + }); + if (actions.length === 0) continue; + + if (!refreshedActors[actor.id]) + refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() }; + refreshedActors[actor.id].refreshed.add( + ...refreshTypes.map(type => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[type].label)) + ); + + if (!updates[item.id]?.system) updates[item.id] = { system: {} }; + + updates[item.id].system = { + ...updates[item.id].system, + ...actions.reduce( + (acc, action) => { + acc.actions[action.id] = { 'uses.value': 0 }; + return acc; + }, + { actions: updates[item.id].system.actions ?? {} } + ) + }; + } + } + + for (let key in updates) { + const update = updates[key]; + await actor.items.get(key).update(update); + } + } + } + + return refreshedActors; + } + + /* -------------------------------------------- */ + /* Application Clicks Actions */ + /* -------------------------------------------- */ + + static async #selectRefreshable(_event, button) { + const { type } = button.dataset; + this.refreshSelections[type].selected = !this.refreshSelections[type].selected; + this.render(); + } + + static async #refreshActors() { + const refreshKeys = Object.keys(this.refreshSelections).filter(key => this.refreshSelections[key].selected); + await this.getRefreshables(refreshKeys); + const types = refreshKeys.map(x => this.refreshSelections[x].label).join(', '); + ui.notifications.info( + game.i18n.format('DAGGERHEART.UI.Notifications.gmMenuRefresh', { + types: `[${types}]` + }) + ); + this.refreshSelections = DaggerheartMenu.#defaultRefreshSelections(); + + const cls = getDocumentClass('ChatMessage'); + const msg = { + user: game.user.id, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/ui/chat/refreshMessage.hbs', + { + types: types + } + ), + title: game.i18n.localize('DAGGERHEART.UI.Chat.refreshMessage.title'), + speaker: cls.getSpeaker() + }; + + cls.create(msg); + + this.render(); + } +} diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index 21762818..b95e50e1 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -112,7 +112,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo if (event.currentTarget.dataset.directDamage) { const config = action.prepareConfig(event); config.hasRoll = false; - action.workflow.get("damage").execute(config, null, true); + action.workflow.get('damage').execute(config, null, true); } else action.use(event); } diff --git a/module/applications/ui/fearTracker.mjs b/module/applications/ui/fearTracker.mjs index a346aa66..2b7c4dac 100644 --- a/module/applications/ui/fearTracker.mjs +++ b/module/applications/ui/fearTracker.mjs @@ -1,4 +1,4 @@ -import { emitAsGM, GMUpdateEvent, socketEvent } from "../../systemRegistration/socket.mjs"; +import { emitAsGM, GMUpdateEvent, socketEvent } from '../../systemRegistration/socket.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -106,6 +106,10 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV } async updateFear(value) { - return emitAsGM(GMUpdateEvent.UpdateFear, game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), value); + return emitAsGM( + GMUpdateEvent.UpdateFear, + game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), + value + ); } } diff --git a/module/applications/ui/itemBrowser.mjs b/module/applications/ui/itemBrowser.mjs index 40884817..a00f8edc 100644 --- a/module/applications/ui/itemBrowser.mjs +++ b/module/applications/ui/itemBrowser.mjs @@ -84,12 +84,10 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { /** @inheritDoc */ async _preRender(context, options) { this.presets = options.presets ?? {}; - + const width = this.presets?.render?.noFolder === true || this.presets?.render?.lite === true ? 600 : 850; - if(this.rendered) - this.setPosition({ width }); - else - options.position.width = width; + if (this.rendered) this.setPosition({ width }); + else options.position.width = width; await super._preRender(context, options); } @@ -100,15 +98,18 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { this.element .querySelectorAll('[data-action="selectFolder"]') - .forEach(element => element.classList.toggle('is-selected', element.dataset.folderId === this.selectedMenu.path.join('.'))); + .forEach(element => + element.classList.toggle('is-selected', element.dataset.folderId === this.selectedMenu.path.join('.')) + ); this._createSearchFilter(); - + this.element.classList.toggle('lite', this.presets?.render?.lite === true); this.element.classList.toggle('no-folder', this.presets?.render?.noFolder === true); this.element.classList.toggle('no-filter', this.presets?.render?.noFilter === true); this.element.querySelectorAll('.folder-list > [data-action="selectFolder"]').forEach(element => { - element.hidden = this.presets.render?.folders?.length && !this.presets.render.folders.includes(element.dataset.folderId); + element.hidden = + this.presets.render?.folders?.length && !this.presets.render.folders.includes(element.dataset.folderId); }); } @@ -157,7 +158,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { : []; folders.push(folder); }); - folders.sort((a, b) => a.label.localeCompare(b.label)) + folders.sort((a, b) => a.label.localeCompare(b.label)); return folders; } @@ -181,8 +182,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { await this.render({ force: true, presets: this.presets }); - if(this.selectedMenu?.data?.type?.length) - this.loadItems(); + if (this.selectedMenu?.data?.type?.length) this.loadItems(); } _replaceHTML(result, content, options) { @@ -194,53 +194,54 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { let loadTimeout = this.toggleLoader(true); const promises = []; - + game.packs.forEach(pack => { promises.push( new Promise(async resolve => { const items = await pack.getDocuments({ type__in: this.selectedMenu?.data?.type }); resolve(items); }) - ) + ); }); - + Promise.all(promises).then(async result => { - this.items = ItemBrowser.sortBy(result.flatMap(r => r), 'name'); + this.items = ItemBrowser.sortBy( + result.flatMap(r => r), + 'name' + ); this.fieldFilter = this._createFieldFilter(); if (this.presets?.filter) { - Object.entries(this.presets.filter).forEach( - ([k, v]) => { - const filter = this.fieldFilter.find(c => c.name === k) - if(filter) filter.value = v.value; - } - ); + Object.entries(this.presets.filter).forEach(([k, v]) => { + const filter = this.fieldFilter.find(c => c.name === k); + if (filter) filter.value = v.value; + }); // await this._onInputFilterBrowser(); } - - const filterList = await foundry.applications.handlebars.renderTemplate('systems/daggerheart/templates/ui/itemBrowser/filterContainer.hbs', + + const filterList = await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/ui/itemBrowser/filterContainer.hbs', { fieldFilter: this.fieldFilter, presets: this.presets, formatChoices: this.formatChoices } ); - + this.element.querySelector('.filter-content .wrapper').innerHTML = filterList; const filterContainer = this.element.querySelector('.filter-header > [data-action="expandContent"]'); - if(this.fieldFilter.length === 0) - filterContainer.setAttribute('disabled', ''); - else - filterContainer.removeAttribute('disabled'); - - const itemList = await foundry.applications.handlebars.renderTemplate('systems/daggerheart/templates/ui/itemBrowser/itemContainer.hbs', + if (this.fieldFilter.length === 0) filterContainer.setAttribute('disabled', ''); + else filterContainer.removeAttribute('disabled'); + + const itemList = await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/ui/itemBrowser/itemContainer.hbs', { items: this.items, menu: this.selectedMenu, formatLabel: this.formatLabel } ); - + this.element.querySelector('.item-list').innerHTML = itemList; this._createFilterInputs(); @@ -255,7 +256,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { toggleLoader(state) { const container = this.element.querySelector('.item-list'); return setTimeout(() => { - container.classList.toggle("loader", state); + container.classList.toggle('loader', state); }, 100); } @@ -376,7 +377,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { for (const li of html.querySelectorAll('.item-container')) { const itemUUID = li.dataset.itemUuid, item = this.items.find(i => i.uuid === itemUUID); - if(!item) continue; + if (!item) continue; const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); if (matchesSearch) this.#filteredItems.browser.search.add(item.id); const { input } = this.#filteredItems.browser; @@ -399,7 +400,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { item = this.items.find(i => i.uuid === itemUUID); if (!item) continue; - + const matchesMenu = this.fieldFilter.length === 0 || this.fieldFilter.every( @@ -503,19 +504,19 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { _canDragStart() { return true; } - + static injectSidebarButton(html) { - if(!game.user.isGM) return; + if (!game.user.isGM) return; const sectionId = html.dataset.tab, - menus = { + menus = { actors: { - folder: "adversaries", + folder: 'adversaries', render: { - folders: ["adversaries", "characters", "environments"] + folders: ['adversaries', 'characters', 'environments'] } }, items: { - folder: "equipments", + folder: 'equipments', render: { noFolder: true } @@ -523,20 +524,20 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { compendium: {} }; - if(Object.keys(menus).includes(sectionId)) { - const headerActions = html.querySelector(".header-actions"); + if (Object.keys(menus).includes(sectionId)) { + const headerActions = html.querySelector('.header-actions'); - const button = document.createElement("button"); - button.type = "button"; - button.classList.add("open-compendium-browser"); + const button = document.createElement('button'); + button.type = 'button'; + button.classList.add('open-compendium-browser'); button.innerHTML = ` - ${game.i18n.localize("DAGGERHEART.UI.Tooltip.compendiumBrowser")} + ${game.i18n.localize('DAGGERHEART.UI.Tooltip.compendiumBrowser')} `; - button.addEventListener("click", event => { + button.addEventListener('click', event => { ui.compendiumBrowser.open(menus[sectionId]); }); - + headerActions.append(button); } } diff --git a/module/config/itemBrowserConfig.mjs b/module/config/itemBrowserConfig.mjs index 046efd8b..e870172f 100644 --- a/module/config/itemBrowserConfig.mjs +++ b/module/config/itemBrowserConfig.mjs @@ -199,8 +199,7 @@ export const typeConfig = { key: 'system.itemFeatures', label: 'DAGGERHEART.GENERAL.features', choices: () => - Object.entries(CONFIG.DH.ITEM.weaponFeatures) - .map(([k, v]) => ({ value: k, label: v.label })), + Object.entries(CONFIG.DH.ITEM.weaponFeatures).map(([k, v]) => ({ value: k, label: v.label })), operator: 'contains3' } ] @@ -241,8 +240,7 @@ export const typeConfig = { key: 'system.itemFeatures', label: 'DAGGERHEART.GENERAL.features', choices: () => - Object.entries(CONFIG.DH.ITEM.armorFeatures) - .map(([k, v]) => ({ value: k, label: v.label })), + Object.entries(CONFIG.DH.ITEM.armorFeatures).map(([k, v]) => ({ value: k, label: v.label })), operator: 'contains3' } ] @@ -372,14 +370,30 @@ export const typeConfig = { label: 'DAGGERHEART.ITEMS.Subclass.spellcastingTrait' } ], + filters: [] + }, + beastforms: { + columns: [ + { + key: 'system.tier', + label: 'DAGGERHEART.GENERAL.Tiers.singular' + }, + { + key: 'system.mainTrait', + label: 'DAGGERHEART.GENERAL.Trait.single' + } + ], filters: [ { key: 'system.linkedClass.uuid', label: 'Class', - choices: (items) => { - const list = items.map(item => ({ value: item.system.linkedClass.uuid, label: item.system.linkedClass.name })); - return list.reduce((a,c) => { - if(!(a.find(i => i.value === c.value))) a.push(c); + choices: items => { + const list = items.map(item => ({ + value: item.system.linkedClass.uuid, + label: item.system.linkedClass.name + })); + return list.reduce((a, c) => { + if (!a.find(i => i.value === c.value)) a.push(c); return a; }, []); } @@ -417,7 +431,7 @@ export const compendiumConfig = { id: 'characters', keys: ['characters'], label: 'DAGGERHEART.UI.ItemBrowser.folders.characters', - type: ['character'], + type: ['character'] // listType: 'characters' }, adversaries: { @@ -431,7 +445,7 @@ export const compendiumConfig = { id: 'ancestries', keys: ['ancestries'], label: 'DAGGERHEART.UI.ItemBrowser.folders.ancestries', - type: ['ancestry'], + type: ['ancestry'] /* folders: { features: { id: 'features', @@ -516,7 +530,7 @@ export const compendiumConfig = { id: 'communities', keys: ['communities'], label: 'DAGGERHEART.UI.ItemBrowser.folders.communities', - type: ['community'], + type: ['community'] /* folders: { features: { id: 'features', @@ -537,7 +551,7 @@ export const compendiumConfig = { keys: ['beastforms'], label: 'DAGGERHEART.UI.ItemBrowser.folders.beastforms', type: ['beastform'], - listType: 'beastforms', + listType: 'beastforms' /* folders: { features: { id: 'features', diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index 0f148aeb..5232cbd9 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -31,20 +31,20 @@ export const gameSettings = { }; export const actionAutomationChoices = { - never: { - id: "never", - label: "Never" + never: { + id: 'never', + label: 'Never' }, showDialog: { - id: "showDialog", - label: "Show Dialog only" + id: 'showDialog', + label: 'Show Dialog only' }, // npcOnly: { - // id: "npcOnly", - // label: "Always for non-characters" + // id: "npcOnly", + // label: "Always for non-characters" // }, always: { - id: "always", - label: "Always" + id: 'always', + label: 'Always' } -} +}; diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index 6e522ceb..93d34700 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -34,8 +34,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel this.extraSchemas.forEach(s => { let clsField = this.getActionField(s); - if (clsField) - schemaFields[s] = new clsField(); + if (clsField) schemaFields[s] = new clsField(); }); return schemaFields; @@ -43,7 +42,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel /** * Create a Map containing each Action step based on fields define in schema. Ordered by Fields order property. - * + * * Each step can be called individually as long as needed config is provided. * Ex: .workflow.get("damage").execute(config) * @returns {Map} @@ -53,8 +52,9 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel this.constructor.extraSchemas.forEach(s => { let clsField = this.constructor.getActionField(s); if (clsField?.execute) { - workflow.set(s, { order: clsField.order, execute: clsField.execute.bind(this) } ); - if( s === "damage" ) workflow.set("applyDamage", { order: 75, execute: clsField.applyDamage.bind(this) } ); + workflow.set(s, { order: clsField.order, execute: clsField.execute.bind(this) }); + if (s === 'damage') + workflow.set('applyDamage', { order: 75, execute: clsField.applyDamage.bind(this) }); } }); return new Map([...workflow.entries()].sort(([aKey, aValue], [bKey, bValue]) => aValue.order - bValue.order)); @@ -64,9 +64,9 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel * Getter returning the workflow property or creating it the first time the property is called */ get workflow() { - if ( this.hasOwnProperty("_workflow") ) return this._workflow; + if (this.hasOwnProperty('_workflow')) return this._workflow; const workflow = Object.freeze(this.defineWorkflow()); - Object.defineProperty(this, "_workflow", {value: workflow, writable: false}); + Object.defineProperty(this, '_workflow', { value: workflow, writable: false }); return workflow; } @@ -117,7 +117,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel /** * Prepare base data based on Action Type & Parent Type - * @param {object} parent + * @param {object} parent * @returns {object} */ static getSourceConfig(parent) { @@ -167,9 +167,9 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel * @param {object} config Config object usually created from prepareConfig method */ async executeWorkflow(config) { - for(const [key, part] of this.workflow) { + for (const [key, part] of this.workflow) { if (Hooks.call(`${CONFIG.DH.id}.pre${key.capitalize()}Action`, this, config) === false) return; - if(await part.execute(config) === false) return; + if ((await part.execute(config)) === false) return; if (Hooks.call(`${CONFIG.DH.id}.post${key.capitalize()}Action`, this, config) === false) return; } } @@ -185,7 +185,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel if (this.chatDisplay) await this.toChat(); let config = this.prepareConfig(event); - if(!config) return; + if (!config) return; if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return; @@ -194,7 +194,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel config = await D20RollDialog.configure(null, config); if (!config) return; } - + // Execute the Action Worflow in order based of schema fields await this.executeWorkflow(config); @@ -206,7 +206,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel /** * Create the basic config common to every action type * @param {Event} event Event from the button used to trigger the Action - * @returns {object} + * @returns {object} */ prepareBaseConfig(event) { const config = { @@ -236,13 +236,12 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel /** * Create the config for that action used for its workflow * @param {Event} event Event from the button used to trigger the Action - * @returns {object} + * @returns {object} */ prepareConfig(event) { const config = this.prepareBaseConfig(event); - for(const clsField of Object.values(this.schema.fields)) { - if (clsField?.prepareConfig) - if(clsField.prepareConfig.call(this, config) === false) return false; + for (const clsField of Object.values(this.schema.fields)) { + if (clsField?.prepareConfig) if (clsField.prepareConfig.call(this, config) === false) return false; } return config; } @@ -260,11 +259,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel * Consume Action configured resources & uses. * That method is only used when we want those resources to be consumed outside of the use method workflow. * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. - * @param {boolean} successCost + * @param {boolean} successCost */ async consume(config, successCost = false) { - await this.workflow.get("cost")?.execute(config, successCost); - await this.workflow.get("uses")?.execute(config, successCost); + await this.workflow.get('cost')?.execute(config, successCost); + await this.workflow.get('uses')?.execute(config, successCost); if (config.roll && !config.roll.success && successCost) { setTimeout(() => { @@ -291,13 +290,13 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel } get hasDamage() { - return this.damage?.parts?.length && this.type !== 'healing' + return this.damage?.parts?.length && this.type !== 'healing'; } get hasHealing() { - return this.damage?.parts?.length && this.type === 'healing' + return this.damage?.parts?.length && this.type === 'healing'; } - + get hasSave() { return !!this.save?.trait; } diff --git a/module/data/chat-message/_modules.mjs b/module/data/chat-message/_modules.mjs index 36c6ee9d..7e301906 100644 --- a/module/data/chat-message/_modules.mjs +++ b/module/data/chat-message/_modules.mjs @@ -1,9 +1,9 @@ -import DHAbilityUse from "./abilityUse.mjs"; -import DHActorRoll from "./actorRoll.mjs"; +import DHAbilityUse from './abilityUse.mjs'; +import DHActorRoll from './actorRoll.mjs'; export const config = { - abilityUse: DHAbilityUse, - adversaryRoll: DHActorRoll, - damageRoll: DHActorRoll, - dualityRoll: DHActorRoll + abilityUse: DHAbilityUse, + adversaryRoll: DHActorRoll, + damageRoll: DHActorRoll, + dualityRoll: DHActorRoll }; diff --git a/module/data/chat-message/actorRoll.mjs b/module/data/chat-message/actorRoll.mjs index 91f44edc..a2cb03f9 100644 --- a/module/data/chat-message/actorRoll.mjs +++ b/module/data/chat-message/actorRoll.mjs @@ -58,10 +58,8 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { const actionActor = this.actionActor, actionItem = this.actionItem; if (!this.source.action) return null; - if(actionItem) - return actionItem.system.actionsList?.find(a => a.id === this.source.action); - else if(actionActor?.system.attack?._id === this.source.action) - return actionActor.system.attack + if (actionItem) return actionItem.system.actionsList?.find(a => a.id === this.source.action); + else if (actionActor?.system.attack?._id === this.source.action) return actionActor.system.attack; return null; } diff --git a/module/data/fields/action/beastformField.mjs b/module/data/fields/action/beastformField.mjs index 62c735d0..0a1caea9 100644 --- a/module/data/fields/action/beastformField.mjs +++ b/module/data/fields/action/beastformField.mjs @@ -1,4 +1,4 @@ -import BeastformDialog from "../../../applications/dialogs/beastformDialog.mjs"; +import BeastformDialog from '../../../applications/dialogs/beastformDialog.mjs'; const fields = foundry.data.fields; @@ -49,14 +49,14 @@ export default class BeastformField extends fields.SchemaField { return await BeastformField.transform.call(this, selected, evolved, hybrid); } - + /** * Update Action Workflow config object. * Must be called within Action context. * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. */ prepareConfig(config) { - if(this.actor.effects.find(x => x.type === 'beastform')) { + if (this.actor.effects.find(x => x.type === 'beastform')) { ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.beastformAlreadyApplied')); return false; } @@ -73,10 +73,10 @@ export default class BeastformField extends fields.SchemaField { /** * TODO by Harry - * @param {*} selectedForm - * @param {*} evolvedData - * @param {*} hybridData - * @returns + * @param {*} selectedForm + * @param {*} evolvedData + * @param {*} hybridData + * @returns */ static async transform(selectedForm, evolvedData, hybridData) { const formData = evolvedData?.form ? evolvedData.form.toObject() : selectedForm.toObject(); diff --git a/module/data/fields/action/damageField.mjs b/module/data/fields/action/damageField.mjs index 816d9cbc..bab46d52 100644 --- a/module/data/fields/action/damageField.mjs +++ b/module/data/fields/action/damageField.mjs @@ -30,8 +30,13 @@ export default class DamageField extends fields.SchemaField { * @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example. */ static async execute(config, messageId = null, force = false) { - if(!this.hasDamage && !this.hasHealing) return; - if((this.hasRoll && DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.never.id) && !force) return; + if (!this.hasDamage && !this.hasHealing) return; + if ( + 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), @@ -51,9 +56,10 @@ export default class DamageField extends fields.SchemaField { }; delete damageConfig.evaluate; - if(DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id) damageConfig.dialog.configure = false; + if (DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id) + damageConfig.dialog.configure = false; if (config.hasSave) config.onSave = damageConfig.onSave = this.save.damageMod; - + damageConfig.source.message = config.message?._id ?? messageId; damageConfig.directDamage = !!damageConfig.source?.message; @@ -61,7 +67,7 @@ export default class DamageField extends fields.SchemaField { // await game.dice3d.waitFor3DAnimationByMessageID(damageConfig.source.message); const damageResult = await CONFIG.Dice.daggerheart.DamageRoll.build(damageConfig); - if(!damageResult) return false; + if (!damageResult) return false; config.damage = damageResult.damage; config.message ??= damageConfig.message; } @@ -70,19 +76,15 @@ export default class DamageField extends fields.SchemaField { * Apply Damage/Healing Action Worflow part. * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. * @param {*[]} targets Arrays of targets to bypass pre-selected ones. - * @param {boolean} force If the method should be executed outside of Action workflow, for ChatMessage button for example. + * @param {boolean} force If the method should be executed outside of Action workflow, for ChatMessage button for example. */ static async applyDamage(config, targets = null, force = false) { targets ??= config.targets.filter(target => target.hit); - if(!config.damage || !targets?.length || (!DamageField.getApplyAutomation() && !force)) return; + if (!config.damage || !targets?.length || (!DamageField.getApplyAutomation() && !force)) return; for (let target of targets) { const actor = fromUuidSync(target.actorId); - if(!actor) continue; - if ( - !config.hasHealing && - config.onSave && - target.saved?.success === true - ) { + if (!actor) continue; + if (!config.hasHealing && config.onSave && target.saved?.success === true) { const mod = CONFIG.DH.ACTIONS.damageOnSave[config.onSave]?.mod ?? 1; Object.entries(config.damage).forEach(([k, v]) => { v.total = 0; @@ -97,17 +99,17 @@ export default class DamageField extends fields.SchemaField { else actor.takeDamage(config.damage, config.isDirect); } } - + /** * Return value or valueAlt from damage part * Must be called within Action context or similar. - * @param {object} part Damage Part + * @param {object} part Damage Part * @param {object} data Action getRollData * @returns Formula value object */ static getFormulaValue(part, data) { let formulaValue = part.value; - + if (data.hasRoll && part.resultBased && data.roll.result.duality === -1) return part.valueAlt; const isAdversary = this.actor.type === 'adversary'; @@ -123,8 +125,8 @@ 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 {object} data Action getRollData - * @returns + * @param {object} data Action getRollData + * @returns */ static formatFormulas(formulas, data) { const formattedFormulas = []; @@ -145,16 +147,25 @@ export default class DamageField extends fields.SchemaField { * @returns {string} Id from settingsConfig.mjs actionAutomationChoices */ static getAutomation() { - return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.players) + return ( + (game.user.isGM && + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.gm) || + (!game.user.isGM && + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.players) + ); } - /** * Return the automation setting for applyDamage method for current user role * @returns {boolean} If applyDamage should be triggered automatically */ static getApplyAutomation() { - return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.players) + return ( + (game.user.isGM && + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.gm) || + (!game.user.isGM && + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.players) + ); } } diff --git a/module/data/fields/action/effectsField.mjs b/module/data/fields/action/effectsField.mjs index 3b8c5e43..0f205d72 100644 --- a/module/data/fields/action/effectsField.mjs +++ b/module/data/fields/action/effectsField.mjs @@ -1,4 +1,4 @@ -import { emitAsGM, GMUpdateEvent } from "../../../systemRegistration/socket.mjs"; +import { emitAsGM, GMUpdateEvent } from '../../../systemRegistration/socket.mjs'; const fields = foundry.data.fields; @@ -25,21 +25,16 @@ export default class EffectsField extends fields.ArrayField { * @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example. */ static async execute(config, targets = null, force = false) { - if(!config.hasEffect) return; + if (!config.hasEffect) return; let message = config.message ?? ui.chat.collection.get(config.parent?._id); - if(!message) { + if (!message) { const roll = new CONFIG.Dice.daggerheart.DHRoll(''); roll._evaluated = true; message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config); } - if(EffectsField.getAutomation() || force) { + if (EffectsField.getAutomation() || force) { targets ??= (message.system?.targets ?? config.targets).filter(t => !config.hasRoll || t.hit); - await emitAsGM( - GMUpdateEvent.UpdateEffect, - EffectsField.applyEffects.bind(this), - targets, - this.uuid - ); + await emitAsGM(GMUpdateEvent.UpdateEffect, EffectsField.applyEffects.bind(this), targets, this.uuid); // EffectsField.applyEffects.call(this, config.targets.filter(t => !config.hasRoll || t.hit)); } } @@ -53,8 +48,7 @@ export default class EffectsField extends fields.ArrayField { if (!this.effects?.length || !targets?.length) return; let effects = this.effects; targets.forEach(async token => { - if (this.hasSave && token.saved.success === true) - effects = this.effects.filter(e => e.onSave === true); + if (this.hasSave && token.saved.success === true) effects = this.effects.filter(e => e.onSave === true); if (!effects.length) return; effects.forEach(async e => { const actor = canvas.tokens.get(token.id)?.actor, @@ -96,6 +90,11 @@ export default class EffectsField extends fields.ArrayField { * @returns {boolean} If execute should be triggered automatically */ static getAutomation() { - return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.effect.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.effect.players) + return ( + (game.user.isGM && + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.effect.gm) || + (!game.user.isGM && + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.effect.players) + ); } } diff --git a/module/data/fields/action/macroField.mjs b/module/data/fields/action/macroField.mjs index 222feb2a..e37a2852 100644 --- a/module/data/fields/action/macroField.mjs +++ b/module/data/fields/action/macroField.mjs @@ -8,7 +8,7 @@ export default class MacroField extends fields.DocumentUUIDField { /** @inheritDoc */ constructor(context = {}) { - super({ type: "Macro" }, context); + super({ type: 'Macro' }, context); } /** diff --git a/module/data/fields/action/rangeField.mjs b/module/data/fields/action/rangeField.mjs index 1237e507..d0bceada 100644 --- a/module/data/fields/action/rangeField.mjs +++ b/module/data/fields/action/rangeField.mjs @@ -1,14 +1,13 @@ const fields = foundry.data.fields; export default class RangeField extends fields.StringField { - /** @inheritDoc */ constructor(context = {}) { const options = { choices: CONFIG.DH.GENERAL.range, required: false, blank: true, - label: "DAGGERHEART.GENERAL.range" + label: 'DAGGERHEART.GENERAL.range' }; super(options, context); } diff --git a/module/data/fields/action/rollField.mjs b/module/data/fields/action/rollField.mjs index cfcfb56b..98a1d5ed 100644 --- a/module/data/fields/action/rollField.mjs +++ b/module/data/fields/action/rollField.mjs @@ -5,7 +5,12 @@ export class DHActionRollData extends foundry.abstract.DataModel { static defineSchema() { return { type: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.GENERAL.rollTypes }), - trait: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.ACTOR.abilities, label: "DAGGERHEART.GENERAL.Trait.single" }), + trait: new fields.StringField({ + nullable: true, + initial: null, + choices: CONFIG.DH.ACTOR.abilities, + label: 'DAGGERHEART.GENERAL.Trait.single' + }), difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }), bonus: new fields.NumberField({ nullable: true, initial: null, integer: true }), advState: new fields.StringField({ @@ -86,14 +91,14 @@ export class DHActionRollData extends foundry.abstract.DataModel { } get rollTrait() { - if(this.parent?.actor?.type !== "character") return null; + if (this.parent?.actor?.type !== 'character') return null; switch (this.type) { case CONFIG.DH.GENERAL.rollTypes.spellcast.id: return this.parent.actor?.system?.spellcastModifierTrait?.key ?? 'agility'; case CONFIG.DH.GENERAL.rollTypes.attack.id: case CONFIG.DH.GENERAL.rollTypes.trait.id: return this.useDefault || !this.trait - ? this.parent.item.system.attack?.roll?.trait ?? 'agility' + ? (this.parent.item.system.attack?.roll?.trait ?? 'agility') : this.trait; default: return null; @@ -118,21 +123,21 @@ export default class RollField extends fields.EmbeddedDataField { * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. */ static async execute(config) { - if(!config.hasRoll) return; + if (!config.hasRoll) return; config = await this.actor.diceRoll(config); - if(!config) return false; + if (!config) return false; } - + /** * Update Action Workflow config object. * Must be called within Action context. * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. */ prepareConfig(config) { - if(!config.hasRoll) return; + if (!config.hasRoll) return; config.dialog.configure = RollField.getAutomation() ? !config.dialog.configure : config.dialog.configure; - + const roll = { baseModifiers: this.roll.getModifier(), label: 'Attack', @@ -152,6 +157,11 @@ export default class RollField extends fields.EmbeddedDataField { * @returns {boolean} If execute should be triggered automatically */ static getAutomation() { - return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.roll.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.roll.players) + return ( + (game.user.isGM && + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.roll.gm) || + (!game.user.isGM && + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.roll.players) + ); } } diff --git a/module/data/fields/action/saveField.mjs b/module/data/fields/action/saveField.mjs index 0003a4d5..b7021135 100644 --- a/module/data/fields/action/saveField.mjs +++ b/module/data/fields/action/saveField.mjs @@ -1,4 +1,4 @@ -import { abilities } from "../../../config/actorConfig.mjs"; +import { abilities } from '../../../config/actorConfig.mjs'; const fields = foundry.data.fields; @@ -33,15 +33,15 @@ export default class SaveField extends fields.SchemaField { * @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example. */ static async execute(config, targets = null, force = false) { - if(!config.hasSave) return; + if (!config.hasSave) return; let message = config.message ?? ui.chat.collection.get(config.parent?._id); - - if(!message) { + + if (!message) { const roll = new CONFIG.Dice.daggerheart.DHRoll(''); roll._evaluated = true; message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config); } - if(SaveField.getAutomation() !== CONFIG.DH.SETTINGS.actionAutomationChoices.never.id || force) { + if (SaveField.getAutomation() !== CONFIG.DH.SETTINGS.actionAutomationChoices.never.id || force) { targets ??= config.targets.filter(t => !config.hasRoll || t.hit); await SaveField.rollAllSave.call(this, targets, config.event, message); } else return false; @@ -52,35 +52,35 @@ export default class SaveField extends fields.SchemaField { * Must be called within Action context. * @param {object[]} targets Array of formatted targets. * @param {Event} event Triggering event - * @param {ChatMessage} message The ChatMessage the triggered button comes from. + * @param {ChatMessage} message The ChatMessage the triggered button comes from. */ static async rollAllSave(targets, event, message) { - if(!targets) return; + if (!targets) return; return new Promise(resolve => { const aPromise = []; targets.forEach(target => { aPromise.push( new Promise(async subResolve => { const actor = fromUuidSync(target.actorId); - if(actor) { - const rollSave = game.user === actor.owner ? - SaveField.rollSave.call(this, actor, event) - : actor.owner - .query('reactionRoll', { - actionId: this.uuid, - actorId: actor.uuid, - event, - message - }); + if (actor) { + const rollSave = + game.user === actor.owner + ? SaveField.rollSave.call(this, actor, event) + : actor.owner.query('reactionRoll', { + actionId: this.uuid, + actorId: actor.uuid, + event, + message + }); const result = await rollSave; await SaveField.updateSaveMessage.call(this, result, message, target.id); subResolve(); } else subResolve(); }) - ) + ); }); Promise.all(aPromise).then(result => resolve()); - }) + }); } /** @@ -93,10 +93,10 @@ export default class SaveField extends fields.SchemaField { static async rollSave(actor, event) { if (!actor) return; const title = actor.isNPC - ? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll') - : game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { - ability: game.i18n.localize(abilities[this.save.trait]?.label) - }), + ? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll') + : game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { + ability: game.i18n.localize(abilities[this.save.trait]?.label) + }), rollConfig = { event, title, @@ -109,7 +109,8 @@ export default class SaveField extends fields.SchemaField { hasRoll: true, data: actor.getRollData() }; - if(SaveField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id) rollConfig.dialog = { configure: false }; + if (SaveField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id) + rollConfig.dialog = { configure: false }; return actor.diceRoll(rollConfig); } @@ -117,31 +118,32 @@ export default class SaveField extends fields.SchemaField { * Update a Roll ChatMessage for a token according to his Reaction Roll result. * @param {object} result Result from the Reaction Roll * @param {object} message ChatMessage to update - * @param {string} targetId Token ID + * @param {string} targetId Token ID */ static async updateSaveMessage(result, message, targetId) { if (!result) return; - const updateMsg = async function(message, targetId, result) { + const updateMsg = async function (message, targetId, result) { // setTimeout(async () => { - const chatMessage = ui.chat.collection.get(message._id), - changes = { - flags: { - [game.system.id]: { - reactionRolls: { - [targetId]: - { - result: result.roll.total, - success: result.roll.success - } + const chatMessage = ui.chat.collection.get(message._id), + changes = { + flags: { + [game.system.id]: { + reactionRolls: { + [targetId]: { + result: result.roll.total, + success: result.roll.success } } } - }; - await chatMessage.update(changes); + } + }; + await chatMessage.update(changes); // }, 100); }; if (game.modules.get('dice-so-nice')?.active) - game.dice3d.waitFor3DAnimationByMessageID(result.message.id ?? result.message._id).then(async () => await updateMsg(message, targetId, result)); + game.dice3d + .waitFor3DAnimationByMessageID(result.message.id ?? result.message._id) + .then(async () => await updateMsg(message, targetId, result)); else await updateMsg(message, targetId, result); } @@ -150,25 +152,29 @@ export default class SaveField extends fields.SchemaField { * @returns {string} Id from settingsConfig.mjs actionAutomationChoices */ static getAutomation() { - return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.save.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.save.players) + return ( + (game.user.isGM && + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.save.gm) || + (!game.user.isGM && + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.save.players) + ); } /** * Send a query to an Actor owner to roll a Reaction Roll then send back the result. - * @param {object} param0 - * @param {string} param0.actionId Action ID - * @param {string} param0.actorId Actor ID - * @param {Event} param0.event Triggering event - * @param {ChatMessage} param0.message Chat Message to update - * @returns + * @param {object} param0 + * @param {string} param0.actionId Action ID + * @param {string} param0.actorId Actor ID + * @param {Event} param0.event Triggering event + * @param {ChatMessage} param0.message Chat Message to update + * @returns */ static rollSaveQuery({ actionId, actorId, event, message }) { return new Promise(async (resolve, reject) => { const actor = await fromUuid(actorId), action = await fromUuid(actionId); if (!actor || !actor?.isOwner) reject(); - SaveField.rollSave.call(action, actor, event, message) - .then(result => resolve(result)); + SaveField.rollSave.call(action, actor, event, message).then(result => resolve(result)); }); } } diff --git a/module/data/fields/action/targetField.mjs b/module/data/fields/action/targetField.mjs index 4499dcc8..486a81f1 100644 --- a/module/data/fields/action/targetField.mjs +++ b/module/data/fields/action/targetField.mjs @@ -1,7 +1,6 @@ const fields = foundry.data.fields; export default class TargetField extends fields.SchemaField { - /** @inheritDoc */ constructor(options = {}, context = {}) { const targetFields = { @@ -21,7 +20,7 @@ export default class TargetField extends fields.SchemaField { * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. */ prepareConfig(config) { - if (!this.target?.type) return config.targets = []; + if (!this.target?.type) return (config.targets = []); config.hasTarget = true; let targets; // If the Action is configured as self-targeted, set targets as the owner. @@ -62,15 +61,11 @@ export default class TargetField extends fields.SchemaField { * @returns {boolean} If both actors respect the provided type. */ static isTargetFriendly(actor, target, type) { - const actorDisposition = actor.token - ? actor.token.disposition - : actor.prototypeToken.disposition, + const actorDisposition = actor.token ? actor.token.disposition : actor.prototypeToken.disposition, targetDisposition = target.document.disposition; return ( - (type === CONFIG.DH.GENERAL.targetTypes.friendly.id && - actorDisposition === targetDisposition) || - (type === CONFIG.DH.GENERAL.targetTypes.hostile.id && - actorDisposition + targetDisposition === 0) + (type === CONFIG.DH.GENERAL.targetTypes.friendly.id && actorDisposition === targetDisposition) || + (type === CONFIG.DH.GENERAL.targetTypes.hostile.id && actorDisposition + targetDisposition === 0) ); } diff --git a/module/data/fields/action/usesField.mjs b/module/data/fields/action/usesField.mjs index d180ddf8..d1f3ebff 100644 --- a/module/data/fields/action/usesField.mjs +++ b/module/data/fields/action/usesField.mjs @@ -7,7 +7,7 @@ export default class UsesField extends fields.SchemaField { * Action Workflow order */ static order = 160; - + /** @inheritDoc */ constructor(options = {}, context = {}) { const usesFields = { @@ -62,7 +62,7 @@ export default class UsesField extends fields.SchemaField { /** * Prepare Uses object for Action Workflow * Must be called within Action context. - * @param {object} uses + * @param {object} uses * @returns {object} */ static calcUses(uses) { @@ -77,7 +77,7 @@ export default class UsesField extends fields.SchemaField { /** * Check if the Action still get atleast one unspent uses. * Must be called within Action context. - * @param {*} uses + * @param {*} uses * @returns {boolean} */ static hasUses(uses) { diff --git a/module/data/settings/Appearance.mjs b/module/data/settings/Appearance.mjs index 4c6e01ea..dfdd17e2 100644 --- a/module/data/settings/Appearance.mjs +++ b/module/data/settings/Appearance.mjs @@ -1,26 +1,27 @@ export default class DhAppearance extends foundry.abstract.DataModel { - static LOCALIZATION_PREFIXES = ["DAGGERHEART.SETTINGS.Appearance"]; + static LOCALIZATION_PREFIXES = ['DAGGERHEART.SETTINGS.Appearance']; static defineSchema() { const { StringField, ColorField, BooleanField, SchemaField } = foundry.data.fields; // helper to create dice style schema - const diceStyle = ({ fg, bg, outline, edge }) => new SchemaField({ - foreground: new ColorField({ required: true, initial: fg }), - background: new ColorField({ required: true, initial: bg }), - outline: new ColorField({ required: true, initial: outline }), - edge: new ColorField({ required: true, initial: edge }), - texture: new StringField({ initial: 'astralsea', required: true, blank: false }), - colorset: new StringField({ initial: 'inspired', required: true, blank: false }), - material: new StringField({ initial: 'metal', required: true, blank: false }), - system: new StringField({ initial: 'standard', required: true, blank: false }) - }); + const diceStyle = ({ fg, bg, outline, edge }) => + new SchemaField({ + foreground: new ColorField({ required: true, initial: fg }), + background: new ColorField({ required: true, initial: bg }), + outline: new ColorField({ required: true, initial: outline }), + edge: new ColorField({ required: true, initial: edge }), + texture: new StringField({ initial: 'astralsea', required: true, blank: false }), + colorset: new StringField({ initial: 'inspired', required: true, blank: false }), + material: new StringField({ initial: 'metal', required: true, blank: false }), + system: new StringField({ initial: 'standard', required: true, blank: false }) + }); return { displayFear: new StringField({ required: true, choices: CONFIG.DH.GENERAL.fearDisplay, - initial: CONFIG.DH.GENERAL.fearDisplay.token.value, + initial: CONFIG.DH.GENERAL.fearDisplay.token.value }), diceSoNice: new SchemaField({ hope: diceStyle({ fg: '#ffffff', bg: '#ffe760', outline: '#000000', edge: '#ffffff' }), @@ -39,7 +40,7 @@ export default class DhAppearance extends foundry.abstract.DataModel { target: new BooleanField() }), hideAttribution: new BooleanField(), - showGenericStatusEffects: new BooleanField({ initial: true }), + showGenericStatusEffects: new BooleanField({ initial: true }) }; } } diff --git a/module/data/settings/Automation.mjs b/module/data/settings/Automation.mjs index da71e899..beefac0b 100644 --- a/module/data/settings/Automation.mjs +++ b/module/data/settings/Automation.mjs @@ -97,13 +97,13 @@ export default class DhAutomation extends foundry.abstract.DataModel { damage: new fields.SchemaField({ gm: new fields.StringField({ required: true, - initial: "never", + initial: 'never', choices: CONFIG.DH.SETTINGS.actionAutomationChoices, label: 'DAGGERHEART.GENERAL.gm' }), players: new fields.StringField({ required: true, - initial: "never", + initial: 'never', choices: CONFIG.DH.SETTINGS.actionAutomationChoices, label: 'DAGGERHEART.GENERAL.player.plurial' }) @@ -111,13 +111,13 @@ export default class DhAutomation extends foundry.abstract.DataModel { save: new fields.SchemaField({ gm: new fields.StringField({ required: true, - initial: "never", + initial: 'never', choices: CONFIG.DH.SETTINGS.actionAutomationChoices, label: 'DAGGERHEART.GENERAL.gm' }), players: new fields.StringField({ required: true, - initial: "never", + initial: 'never', choices: CONFIG.DH.SETTINGS.actionAutomationChoices, label: 'DAGGERHEART.GENERAL.player.plurial' }) diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index 4d293d9d..534867f8 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -47,8 +47,7 @@ export default class DamageRoll extends DHRoll { ); } await super.buildPost(roll, config, message); - if (config.source?.message) - chatMessage.update({ 'system.damage': config.damage }); + if (config.source?.message) chatMessage.update({ 'system.damage': config.damage }); } static unifyDamageRoll(rolls) { diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index 0dcdd316..3865710a 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -28,7 +28,7 @@ export default class DHRoll extends Roll { static async buildConfigure(config = {}, message = {}) { config.hooks = [...this.getHooks(), '']; config.dialog ??= {}; - + for (const hook of config.hooks) { if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null; } @@ -46,7 +46,10 @@ export default class DHRoll extends Roll { } for (const hook of config.hooks) { - if (Hooks.call(`${CONFIG.DH.id}.post${hook.capitalize()}RollConfiguration`, roll, config, message) === false) return []; + if ( + Hooks.call(`${CONFIG.DH.id}.post${hook.capitalize()}RollConfiguration`, roll, config, message) === false + ) + return []; } return roll; } @@ -91,7 +94,7 @@ export default class DHRoll extends Roll { system: config, rolls: [roll] }; - + config.selectedRollMode ??= game.settings.get('core', 'rollMode'); if (roll._evaluated) { diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index 93ac231e..8fedc368 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -153,10 +153,13 @@ export default class DualityRoll extends D20Roll { applyBaseBonus() { const modifiers = super.applyBaseBonus(); - + if (this.options.roll.trait && this.data.traits?.[this.options.roll.trait]) modifiers.unshift({ - label: this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id ? "DAGGERHEART.CONFIG.RollTypes.spellcast.name" : `DAGGERHEART.CONFIG.Traits.${this.options.roll.trait}.name`, + label: + this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id + ? 'DAGGERHEART.CONFIG.RollTypes.spellcast.name' + : `DAGGERHEART.CONFIG.Traits.${this.options.roll.trait}.name`, value: this.data.traits[this.options.roll.trait].value }); diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index 1a619a9c..d7476395 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -1,4 +1,4 @@ -import { emitAsGM, GMUpdateEvent } from "../systemRegistration/socket.mjs"; +import { emitAsGM, GMUpdateEvent } from '../systemRegistration/socket.mjs'; export default class DhpChatMessage extends foundry.documents.ChatMessage { targetHook = null; @@ -104,11 +104,11 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { }); if (itemDesc && autoExpandRoll.desc) itemDesc.setAttribute('open', ''); } - - if(!this.isAuthor && !this.speakerActor?.isOwner) { - const applyButtons = html.querySelector(".apply-buttons"); + + if (!this.isAuthor && !this.speakerActor?.isOwner) { + const applyButtons = html.querySelector('.apply-buttons'); applyButtons?.remove(); - const buttons = html.querySelectorAll(".ability-card-footer > .ability-use-button"); + const buttons = html.querySelectorAll('.ability-card-footer > .ability-use-button'); buttons.forEach(b => b.remove()); } } @@ -125,7 +125,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { html.querySelectorAll('.target-save').forEach(element => element.addEventListener('click', this.onRollSave.bind(this)) ); - + html.querySelectorAll('.roll-all-save-button').forEach(element => element.addEventListener('click', this.onRollAllSave.bind(this)) ); @@ -149,7 +149,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { event.stopPropagation(); const config = foundry.utils.deepClone(this.system); config.event = event; - this.system.action?.workflow.get("damage")?.execute(config, this._id, true); + this.system.action?.workflow.get('damage')?.execute(config, this._id, true); } async onApplyDamage(event) { @@ -171,9 +171,9 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { if (targets.length === 0) return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm')); - + this.consumeOnSuccess(); - this.system.action?.workflow.get("applyDamage")?.execute(config, targets, true); + this.system.action?.workflow.get('applyDamage')?.execute(config, targets, true); } async onRollSave(event) { @@ -187,7 +187,12 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { game.system.api.fields.ActionFields.SaveField.rollSave.call(action, token.actor, event).then(result => emitAsGM( GMUpdateEvent.UpdateSaveMessage, - game.system.api.fields.ActionFields.SaveField.updateSaveMessage.bind(action, result, this, token.id), + game.system.api.fields.ActionFields.SaveField.updateSaveMessage.bind( + action, + result, + this, + token.id + ), { action: action.uuid, message: this._id, @@ -205,7 +210,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { const targets = this.system.hitTargets, config = foundry.utils.deepClone(this.system); config.event = event; - this.system.action?.workflow.get("save")?.execute(config, targets, true); + this.system.action?.workflow.get('save')?.execute(config, targets, true); } async onApplyEffect(event) { @@ -216,16 +221,15 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { if (targets.length === 0) ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm')); this.consumeOnSuccess(); - this.system.action?.workflow.get("effects")?.execute(config, targets, true); + this.system.action?.workflow.get('effects')?.execute(config, targets, true); } filterPermTargets(targets) { - return targets.filter(t => fromUuidSync(t.actorId)?.canUserModify(game.user, "update")) + return targets.filter(t => fromUuidSync(t.actorId)?.canUserModify(game.user, 'update')); } consumeOnSuccess() { - if (!this.system.successConsumed && !this.targetSelection) - this.system.action?.consume(this.system, true); + if (!this.system.successConsumed && !this.targetSelection) this.system.action?.consume(this.system, true); } hoverTarget(event) { diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 8492f068..33daf52a 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -32,7 +32,7 @@ export default class DHItem extends foundry.documents.Item { /** @inheritDoc */ static migrateData(source) { - if(source.system?.attack && !source.system.attack.type) source.system.attack.type = "attack"; + if (source.system?.attack && !source.system.attack.type) source.system.attack.type = 'attack'; return super.migrateData(source); } diff --git a/module/helpers/handlebarsHelper.mjs b/module/helpers/handlebarsHelper.mjs index 83220307..e6c1a2f0 100644 --- a/module/helpers/handlebarsHelper.mjs +++ b/module/helpers/handlebarsHelper.mjs @@ -76,10 +76,10 @@ export default class RegisterHandlebarsHelpers { /** * Pluralize helper that returns the appropriate localized string based on count - * @param {number} count - The number to check for plurality + * @param {number} count - The number to check for plurality * @param {string} baseKey - The base localization key (e.g., "DAGGERHEART.GENERAL.Target") * @returns {string} The localized singular or plural string - * + * * Usage: {{pluralize currentTargets.length "DAGGERHEART.GENERAL.Target"}} * Returns: "Target" if count is exactly 1, "Targets" if count is 0, 2+, or invalid */ diff --git a/module/systemRegistration/handlebars.mjs b/module/systemRegistration/handlebars.mjs index fd569499..24047827 100644 --- a/module/systemRegistration/handlebars.mjs +++ b/module/systemRegistration/handlebars.mjs @@ -34,10 +34,7 @@ export const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/ui/chat/parts/damage-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/target-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/button-part.hbs', - 'systems/daggerheart/templates/ui/itemBrowser/itemContainer.hbs', - - 'systems/daggerheart/templates/scene/dh-config.hbs', - + 'systems/daggerheart/templates/scene/dh-config.hbs' ]); }; diff --git a/module/systemRegistration/socket.mjs b/module/systemRegistration/socket.mjs index f3f9629b..f75c7b36 100644 --- a/module/systemRegistration/socket.mjs +++ b/module/systemRegistration/socket.mjs @@ -38,8 +38,7 @@ export const registerSocketHooks = () => { const document = data.uuid ? await fromUuid(data.uuid) : null; switch (data.action) { case GMUpdateEvent.UpdateDocument: - if (document && data.update) - await document.update(data.update); + if (document && data.update) await document.update(data.update); break; case GMUpdateEvent.UpdateEffect: if (document && data.update) diff --git a/pull_request_template.md b/pull_request_template.md index c1b8cbfa..263eb5b8 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -1,10 +1,11 @@ --- name: Pull Request about: Create a new pull request -title: "[PR] " +title: '[PR] ' labels: pr assignees: '' --- + Is this a community PR? Please go to preview tab and click [here](?expand=1&template=community_pull_request_template.md). If not, delete this line. ## Description diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index e4a2128c..65e825a9 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -603,7 +603,7 @@ display: flex; justify-content: space-between; align-items: center; - gap: .25rem .5rem; + gap: 0.25rem 0.5rem; flex-wrap: wrap; label { @@ -620,7 +620,7 @@ &.setting-two-values { display: grid; grid-template-columns: repeat(3, 1fr); - gap: .25rem .5rem; + gap: 0.25rem 0.5rem; .form-group { justify-content: end; diff --git a/styles/less/global/global.less b/styles/less/global/global.less index 4c06d42b..7e60dffc 100644 --- a/styles/less/global/global.less +++ b/styles/less/global/global.less @@ -29,11 +29,11 @@ overflow: hidden !important; div { - opacity: .5; + opacity: 0.5; } &:before { - font-family: "Font Awesome 6 Pro"; + font-family: 'Font Awesome 6 Pro'; content: '\f110'; position: absolute; height: 100%; @@ -41,11 +41,13 @@ display: flex; align-items: center; justify-content: center; - animation: spinner 1.5s linear infinite; + animation: spinner 1.5s linear infinite; } } @keyframes spinner { - to { transform: rotate(360deg); } + to { + transform: rotate(360deg); + } } -} \ No newline at end of file +} diff --git a/styles/less/global/sheet.less b/styles/less/global/sheet.less index 08e2668f..6f77a481 100755 --- a/styles/less/global/sheet.less +++ b/styles/less/global/sheet.less @@ -15,7 +15,7 @@ body.game:is(.performance-low, .noblur) { .themed.theme-dark.application.daggerheart.sheet.dh-style, &.theme-dark .application.daggerheart { background: @dark-blue; - }; + } } .application.sheet.dh-style { diff --git a/styles/less/sheets/actors/environment/potentialAdversaries.less b/styles/less/sheets/actors/environment/potentialAdversaries.less index 6fd7af65..274362a5 100644 --- a/styles/less/sheets/actors/environment/potentialAdversaries.less +++ b/styles/less/sheets/actors/environment/potentialAdversaries.less @@ -14,4 +14,4 @@ scrollbar-color: light-dark(@dark-blue, @golden) transparent; } } -} \ No newline at end of file +} diff --git a/styles/less/ui/chat/refresh-message.less b/styles/less/ui/chat/refresh-message.less new file mode 100644 index 00000000..2fce189b --- /dev/null +++ b/styles/less/ui/chat/refresh-message.less @@ -0,0 +1,13 @@ +.daggerheart.chat.refresh-message { + header { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + + .subtitle { + font-size: 18; + font-weight: bold; + } + } +} diff --git a/styles/less/ui/index.less b/styles/less/ui/index.less index 49d1e009..8b0c53f6 100644 --- a/styles/less/ui/index.less +++ b/styles/less/ui/index.less @@ -2,6 +2,7 @@ @import './chat/action.less'; @import './chat/chat.less'; @import './chat/downtime.less'; +@import './chat/refresh-message.less'; @import './chat/sheet.less'; @import './combat-sidebar/combat-sidebar.less'; @@ -19,6 +20,8 @@ @import './resources/resources.less'; @import './settings/settings.less'; - @import './settings/homebrew-settings/domains.less'; @import './settings/homebrew-settings/types.less'; + +@import './sidebar/tabs.less'; +@import './sidebar/daggerheartMenu.less'; diff --git a/styles/less/ui/item-browser/item-browser.less b/styles/less/ui/item-browser/item-browser.less index 5df0482a..23844128 100644 --- a/styles/less/ui/item-browser/item-browser.less +++ b/styles/less/ui/item-browser/item-browser.less @@ -395,7 +395,7 @@ text-align: center; font-weight: bold; } - + .hint { flex: unset; } @@ -409,7 +409,8 @@ &.lite, &.no-folder { - .compendium-sidebar, .menu-path { + .compendium-sidebar, + .menu-path { display: none; } } diff --git a/styles/less/ui/settings/settings.less b/styles/less/ui/settings/settings.less index 67fe7718..788db394 100644 --- a/styles/less/ui/settings/settings.less +++ b/styles/less/ui/settings/settings.less @@ -4,7 +4,7 @@ fieldset { display: flex; flex-direction: column; - gap: .5rem; + gap: 0.5rem; &.two-columns { display: grid; @@ -127,4 +127,4 @@ text-align: center; } } -} \ No newline at end of file +} diff --git a/styles/less/ui/sidebar/daggerheartMenu.less b/styles/less/ui/sidebar/daggerheartMenu.less new file mode 100644 index 00000000..e975954c --- /dev/null +++ b/styles/less/ui/sidebar/daggerheartMenu.less @@ -0,0 +1,38 @@ +.tab.sidebar-tab.daggerheartMenu-sidebar { + padding: 0 4px; + + .menu-refresh-container { + display: flex; + flex-direction: column; + gap: 8px; + + .menu-refresh-inner-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + + .experience-chip { + display: flex; + align-items: center; + border-radius: 5px; + width: fit-content; + gap: 5px; + cursor: pointer; + padding: 5px; + background: light-dark(@dark-blue-10, @golden-10); + color: light-dark(@dark-blue, @golden); + + .label { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 17px; + } + + &.selected { + background: light-dark(@dark-blue-40, @golden-40); + } + } + } + } +} diff --git a/styles/less/ui/sidebar/tabs.less b/styles/less/ui/sidebar/tabs.less new file mode 100644 index 00000000..073d3ef3 --- /dev/null +++ b/styles/less/ui/sidebar/tabs.less @@ -0,0 +1,8 @@ +#interface #ui-right #sidebar { + menu li button { + img { + width: 22px; + max-width: unset; + } + } +} diff --git a/templates/sidebar/daggerheart-menu/main.hbs b/templates/sidebar/daggerheart-menu/main.hbs new file mode 100644 index 00000000..6f31f165 --- /dev/null +++ b/templates/sidebar/daggerheart-menu/main.hbs @@ -0,0 +1,22 @@ +
+
+ {{localize "Refresh Features"}} + + +
+
\ No newline at end of file diff --git a/templates/sidebar/tabs.hbs b/templates/sidebar/tabs.hbs new file mode 100644 index 00000000..9063ac5d --- /dev/null +++ b/templates/sidebar/tabs.hbs @@ -0,0 +1,18 @@ + diff --git a/templates/ui/chat/refreshMessage.hbs b/templates/ui/chat/refreshMessage.hbs new file mode 100644 index 00000000..a29baa20 --- /dev/null +++ b/templates/ui/chat/refreshMessage.hbs @@ -0,0 +1,6 @@ +
+
+
{{localize "DAGGERHEART.UI.Chat.refreshMessage.header"}}
+
{{types}}
+
+
\ No newline at end of file From 2176038ec6a9939f52eef2dfe5c1a7395d684dc6 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sun, 7 Sep 2025 00:47:21 +0200 Subject: [PATCH 16/18] Added homebrew for armor and weapon fatures (#1166) Co-authored-by: Chris Ryan --- daggerheart.mjs | 2 + lang/en.json | 5 + .../settings/homebrewSettings.mjs | 65 +++-- .../applications/sheets-configs/_module.mjs | 3 +- .../sheets-configs/action-config.mjs | 2 +- .../sheets-configs/activeEffectConfig.mjs | 7 + .../setting-active-effect-config.mjs | 227 ++++++++++++++++++ ...eConfig.mjs => setting-feature-config.mjs} | 68 +++++- module/applications/sheets/items/armor.mjs | 2 +- module/applications/sheets/items/weapon.mjs | 2 +- module/config/itemConfig.mjs | 56 +++++ module/data/activeEffect/baseEffect.mjs | 20 ++ module/data/item/armor.mjs | 26 +- module/data/item/weapon.mjs | 18 +- module/data/settings/Homebrew.mjs | 30 ++- .../settings/downtime-config/effects.hbs | 15 ++ templates/settings/downtime-config/main.hbs | 10 +- .../homebrew-settings/itemFeatures.hbs | 35 +++ templates/sheets/activeEffect/changes.hbs | 36 +-- templates/sheets/activeEffect/settings.hbs | 8 +- 20 files changed, 560 insertions(+), 77 deletions(-) create mode 100644 module/applications/sheets-configs/setting-active-effect-config.mjs rename module/applications/sheets-configs/{downtimeConfig.mjs => setting-feature-config.mjs} (66%) create mode 100644 templates/settings/downtime-config/effects.hbs create mode 100644 templates/settings/homebrew-settings/itemFeatures.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index f5f5e303..d7aba401 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -1,5 +1,6 @@ import { SYSTEM } from './module/config/system.mjs'; import * as applications from './module/applications/_module.mjs'; +import * as data from './module/data/_module.mjs'; import * as models from './module/data/_module.mjs'; import * as documents from './module/documents/_module.mjs'; import * as dice from './module/dice/_module.mjs'; @@ -26,6 +27,7 @@ Hooks.once('init', () => { CONFIG.DH = SYSTEM; game.system.api = { applications, + data, models, documents, dice, diff --git a/lang/en.json b/lang/en.json index 8de962bd..85e941dc 100755 --- a/lang/en.json +++ b/lang/en.json @@ -1917,6 +1917,7 @@ "roll": "Roll", "rules": "Rules", "types": "Types", + "itemFeatures": "Item Features", "questions": "Questions" }, "Tiers": { @@ -1934,6 +1935,7 @@ "amount": "Amount", "any": "Any", "armor": "Armor", + "armorFeatures": "Armor Features", "armors": "Armors", "armorScore": "Armor Score", "activeEffects": "Active Effects", @@ -2046,6 +2048,7 @@ "used": "Used", "uses": "Uses", "value": "Value", + "weaponFeatures": "Weapon Features", "weapons": "Weapons", "withThing": "With {thing}" }, @@ -2276,7 +2279,9 @@ }, "Homebrew": { "newDowntimeMove": "Downtime Move", + "newFeature": "New ItemFeature", "downtimeMoves": "Downtime Moves", + "itemFeatures": "Item Features", "nrChoices": "# Moves Per Rest", "resetMovesTitle": "Reset {type} Downtime Moves", "resetMovesText": "Are you sure you want to reset?", diff --git a/module/applications/settings/homebrewSettings.mjs b/module/applications/settings/homebrewSettings.mjs index c2ac4a89..e880f7ee 100644 --- a/module/applications/settings/homebrewSettings.mjs +++ b/module/applications/settings/homebrewSettings.mjs @@ -53,6 +53,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli settings: { template: 'systems/daggerheart/templates/settings/homebrew-settings/settings.hbs' }, domains: { template: 'systems/daggerheart/templates/settings/homebrew-settings/domains.hbs' }, types: { template: 'systems/daggerheart/templates/settings/homebrew-settings/types.hbs' }, + itemTypes: { template: 'systems/daggerheart/templates/settings/homebrew-settings/itemFeatures.hbs' }, downtime: { template: 'systems/daggerheart/templates/settings/homebrew-settings/downtime.hbs' }, footer: { template: 'systems/daggerheart/templates/settings/homebrew-settings/footer.hbs' } }; @@ -60,7 +61,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli /** @inheritdoc */ static TABS = { main: { - tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'types' }, { id: 'downtime' }], + tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'types' }, { id: 'itemFeatures' }, { id: 'downtime' }], initial: 'settings', labelPrefix: 'DAGGERHEART.GENERAL.Tabs' } @@ -115,33 +116,53 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli } static async addItem(_, target) { - await this.settings.updateSource({ - [`restMoves.${target.dataset.type}.moves.${foundry.utils.randomID()}`]: { - name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newDowntimeMove'), - img: 'icons/magic/life/cross-worn-green.webp', - description: '', - actions: [] - } - }); + const { type } = target.dataset; + if (['shortRest', 'longRest'].includes(type)) { + await this.settings.updateSource({ + [`restMoves.${type}.moves.${foundry.utils.randomID()}`]: { + name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newDowntimeMove'), + img: 'icons/magic/life/cross-worn-green.webp', + description: '', + actions: [] + } + }); + } else if (['armorFeatures', 'weaponFeatures'].includes(type)) { + await this.settings.updateSource({ + [`itemFeatures.${type}.${foundry.utils.randomID()}`]: { + name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newFeature'), + img: 'icons/magic/life/cross-worn-green.webp', + description: '', + actions: [], + effects: [] + } + }); + } + this.render(); } static async editItem(_, target) { - const move = this.settings.restMoves[target.dataset.type].moves[target.dataset.id]; - const path = `restMoves.${target.dataset.type}.moves.${target.dataset.id}`; - const editedMove = await game.system.api.applications.sheetConfigs.DowntimeConfig.configure( - move, - path, - this.settings - ); - if (!editedMove) return; + const { type, id } = target.dataset; + const isDowntime = ['shortRest', 'longRest'].includes(type); + const path = isDowntime ? `restMoves.${type}.moves.${id}` : `itemFeatures.${type}.${id}`; + const featureBase = isDowntime ? this.settings.restMoves[type].moves[id] : this.settings.itemFeatures[type][id]; - await this.updateAction.bind(this)(editedMove, target.dataset.type, target.dataset.id); + const editedBase = await game.system.api.applications.sheetConfigs.SettingFeatureConfig.configure( + featureBase, + path, + this.settings, + { hasIcon: isDowntime, hasEffects: !isDowntime } + ); + if (!editedBase) return; + + await this.updateAction.bind(this)(editedBase, target.dataset.type, target.dataset.id); } async updateAction(data, type, id) { + const isDowntime = ['shortRest', 'longRest'].includes(type); + const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`; await this.settings.updateSource({ - [`restMoves.${type}.moves.${id}`]: { + [`${path}.${id}`]: { actions: data.actions, name: data.name, icon: data.icon, @@ -149,12 +170,16 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli description: data.description } }); + this.render(); } static async removeItem(_, target) { + const { type, id } = target.dataset; + const isDowntime = ['shortRest', 'longRest'].includes(type); + const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`; await this.settings.updateSource({ - [`restMoves.${target.dataset.type}.moves.-=${target.dataset.id}`]: null + [`${path}.-=${id}`]: null }); this.render(); } diff --git a/module/applications/sheets-configs/_module.mjs b/module/applications/sheets-configs/_module.mjs index ed062163..a8a625d0 100644 --- a/module/applications/sheets-configs/_module.mjs +++ b/module/applications/sheets-configs/_module.mjs @@ -2,7 +2,8 @@ export { default as ActionConfig } from './action-config.mjs'; export { default as CharacterSettings } from './character-settings.mjs'; export { default as AdversarySettings } from './adversary-settings.mjs'; export { default as CompanionSettings } from './companion-settings.mjs'; -export { default as DowntimeConfig } from './downtimeConfig.mjs'; +export { default as SettingActiveEffectConfig } from './setting-active-effect-config.mjs'; +export { default as SettingFeatureConfig } from './setting-feature-config.mjs'; export { default as EnvironmentSettings } from './environment-settings.mjs'; export { default as ActiveEffectConfig } from './activeEffectConfig.mjs'; export { default as DhTokenConfig } from './token-config.mjs'; diff --git a/module/applications/sheets-configs/action-config.mjs b/module/applications/sheets-configs/action-config.mjs index 96b6cc48..0f92baa1 100644 --- a/module/applications/sheets-configs/action-config.mjs +++ b/module/applications/sheets-configs/action-config.mjs @@ -138,7 +138,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { }; } - if (this.action.parent.metadata.isQuantifiable) { + if (this.action.parent.metadata?.isQuantifiable) { options.quantity = { label: 'DAGGERHEART.GENERAL.itemQuantity', group: 'Global' diff --git a/module/applications/sheets-configs/activeEffectConfig.mjs b/module/applications/sheets-configs/activeEffectConfig.mjs index 25f7d2b5..6a466c55 100644 --- a/module/applications/sheets-configs/activeEffectConfig.mjs +++ b/module/applications/sheets-configs/activeEffectConfig.mjs @@ -96,6 +96,13 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac }); } + async _prepareContext(options) { + const context = await super._prepareContext(options); + context.systemFields = context.document.system.schema.fields; + + return context; + } + async _preparePartContext(partId, context) { const partContext = await super._preparePartContext(partId, context); switch (partId) { diff --git a/module/applications/sheets-configs/setting-active-effect-config.mjs b/module/applications/sheets-configs/setting-active-effect-config.mjs new file mode 100644 index 00000000..9b57b47a --- /dev/null +++ b/module/applications/sheets-configs/setting-active-effect-config.mjs @@ -0,0 +1,227 @@ +import autocomplete from 'autocompleter'; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +export default class SettingActiveEffectConfig extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(effect) { + super({}); + + this.effect = foundry.utils.deepClone(effect); + const ignoredActorKeys = ['config', 'DhEnvironment']; + this.changeChoices = Object.keys(game.system.api.models.actors).reduce((acc, key) => { + if (!ignoredActorKeys.includes(key)) { + const model = game.system.api.models.actors[key]; + const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model); + const group = game.i18n.localize(model.metadata.label); + const choices = CONFIG.Token.documentClass + .getTrackedAttributeChoices(attributes, model) + .map(x => ({ ...x, group: group })); + acc.push(...choices); + } + return acc; + }, []); + } + + static DEFAULT_OPTIONS = { + classes: ['daggerheart', 'sheet', 'dh-style', 'active-effect-config'], + tag: 'form', + position: { + width: 560 + }, + form: { + submitOnChange: false, + closeOnSubmit: false, + handler: SettingActiveEffectConfig.#onSubmit + }, + actions: { + editImage: SettingActiveEffectConfig.#editImage, + addChange: SettingActiveEffectConfig.#addChange, + deleteChange: SettingActiveEffectConfig.#deleteChange + } + }; + + static PARTS = { + header: { template: 'systems/daggerheart/templates/sheets/activeEffect/header.hbs' }, + tabs: { template: 'templates/generic/tab-navigation.hbs' }, + details: { template: 'systems/daggerheart/templates/sheets/activeEffect/details.hbs', scrollable: [''] }, + settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' }, + changes: { + template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs', + scrollable: ['ol[data-changes]'] + }, + footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' } + }; + + static TABS = { + sheet: { + tabs: [ + { id: 'details', icon: 'fa-solid fa-book' }, + { id: 'settings', icon: 'fa-solid fa-bars', label: 'DAGGERHEART.GENERAL.Tabs.settings' }, + { id: 'changes', icon: 'fa-solid fa-gears' } + ], + initial: 'details', + labelPrefix: 'EFFECT.TABS' + } + }; + + /**@inheritdoc */ + async _onFirstRender(context, options) { + await super._onFirstRender(context, options); + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.source = this.effect; + context.fields = game.system.api.documents.DhActiveEffect.schema.fields; + context.systemFields = game.system.api.data.activeEffects.BaseEffect._schema.fields; + + return context; + } + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + const changeChoices = this.changeChoices; + + htmlElement.querySelectorAll('.effect-change-input').forEach(element => { + autocomplete({ + input: element, + fetch: function (text, update) { + if (!text) { + update(changeChoices); + } else { + text = text.toLowerCase(); + var suggestions = changeChoices.filter(n => n.label.toLowerCase().includes(text)); + update(suggestions); + } + }, + render: function (item, search) { + const label = game.i18n.localize(item.label); + const matchIndex = label.toLowerCase().indexOf(search); + + const beforeText = label.slice(0, matchIndex); + const matchText = label.slice(matchIndex, matchIndex + search.length); + const after = label.slice(matchIndex + search.length, label.length); + + const element = document.createElement('li'); + element.innerHTML = `${beforeText}${matchText ? `${matchText}` : ''}${after}`; + if (item.hint) { + element.dataset.tooltip = game.i18n.localize(item.hint); + } + + return element; + }, + renderGroup: function (label) { + const itemElement = document.createElement('div'); + itemElement.textContent = game.i18n.localize(label); + return itemElement; + }, + onSelect: function (item) { + element.value = `system.${item.value}`; + }, + click: e => e.fetch(), + customize: function (_input, _inputRect, container) { + container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ; + }, + minLength: 0 + }); + }); + } + + async _preparePartContext(partId, context) { + if (partId in context.tabs) context.tab = context.tabs[partId]; + switch (partId) { + case 'details': + context.isActorEffect = false; + context.isItemEffect = true; + const useGeneric = game.settings.get( + CONFIG.DH.id, + CONFIG.DH.SETTINGS.gameSettings.appearance + ).showGenericStatusEffects; + if (!useGeneric) { + context.statuses = Object.values(CONFIG.DH.GENERAL.conditions).map(status => ({ + value: status.id, + label: game.i18n.localize(status.name) + })); + } + break; + case 'changes': + context.modes = Object.entries(CONST.ACTIVE_EFFECT_MODES).reduce((modes, [key, value]) => { + modes[value] = game.i18n.localize(`EFFECT.MODE_${key}`); + return modes; + }, {}); + + context.priorities = ActiveEffectConfig.DEFAULT_PRIORITIES; + break; + } + + return context; + } + + static async #onSubmit(event, form, formData) { + this.data = foundry.utils.expandObject(formData.object); + this.close(); + } + + /** + * Edit a Document image. + * @this {DocumentSheetV2} + * @type {ApplicationClickAction} + */ + static async #editImage(_event, target) { + if (target.nodeName !== 'IMG') { + throw new Error('The editImage action is available only for IMG elements.'); + } + + const attr = target.dataset.edit; + const current = foundry.utils.getProperty(this.effect, attr); + const fp = new FilePicker.implementation({ + current, + type: 'image', + callback: path => (target.src = path), + position: { + top: this.position.top + 40, + left: this.position.left + 10 + } + }); + + await fp.browse(); + } + + /** + * Add a new change to the effect's changes array. + * @this {ActiveEffectConfig} + * @type {ApplicationClickAction} + */ + static async #addChange() { + const submitData = foundry.utils.expandObject(new FormDataExtended(this.form).object); + const changes = Object.values(submitData.changes ?? {}); + changes.push({}); + + this.effect.changes = changes; + this.render(); + } + + /** + * Delete a change from the effect's changes array. + * @this {ActiveEffectConfig} + * @type {ApplicationClickAction} + */ + static async #deleteChange(event) { + const submitData = foundry.utils.expandObject(new FormDataExtended(this.form).object); + const changes = Object.values(submitData.changes); + const row = event.target.closest('li'); + const index = Number(row.dataset.index) || 0; + changes.splice(index, 1); + + this.effect.changes = changes; + this.render(); + } + + static async configure(effect, options = {}) { + return new Promise(resolve => { + const app = new this(effect, options); + app.addEventListener('close', () => resolve(app.data), { once: true }); + app.render({ force: true }); + }); + } +} diff --git a/module/applications/sheets-configs/downtimeConfig.mjs b/module/applications/sheets-configs/setting-feature-config.mjs similarity index 66% rename from module/applications/sheets-configs/downtimeConfig.mjs rename to module/applications/sheets-configs/setting-feature-config.mjs index 80aab900..e775f93d 100644 --- a/module/applications/sheets-configs/downtimeConfig.mjs +++ b/module/applications/sheets-configs/setting-feature-config.mjs @@ -3,8 +3,8 @@ import DHActionConfig from './action-config.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; -export default class DowntimeConfig extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(move, movePath, settings, options) { +export default class SettingFeatureConfig extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(move, movePath, settings, optionalParts, options) { super(options); this.move = move; @@ -12,6 +12,10 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati this.movePath = movePath; this.actionsPath = `${movePath}.actions`; this.settings = settings; + + const { hasIcon, hasEffects } = optionalParts; + this.hasIcon = hasIcon; + this.hasEffects = hasEffects; } get title() { @@ -30,6 +34,7 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati addItem: this.addItem, editItem: this.editItem, removeItem: this.removeItem, + addEffect: this.addEffect, resetMoves: this.resetMoves, saveForm: this.saveForm }, @@ -41,13 +46,14 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, main: { template: 'systems/daggerheart/templates/settings/downtime-config/main.hbs' }, actions: { template: 'systems/daggerheart/templates/settings/downtime-config/actions.hbs' }, + effects: { template: 'systems/daggerheart/templates/settings/downtime-config/effects.hbs' }, footer: { template: 'systems/daggerheart/templates/settings/downtime-config/footer.hbs' } }; /** @inheritdoc */ static TABS = { primary: { - tabs: [{ id: 'main' }, { id: 'actions' }], + tabs: [{ id: 'main' }, { id: 'actions' }, { id: 'effects' }], initial: 'main', labelPrefix: 'DAGGERHEART.GENERAL.Tabs' } @@ -55,6 +61,9 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati async _prepareContext(_options) { const context = await super._prepareContext(_options); + context.tabs = this._filterTabs(context.tabs); + context.hasIcon = this.hasIcon; + context.hasEffects = this.hasEffects; context.move = this.move; context.move.enrichedDescription = await foundry.applications.ux.TextEditor.enrichHTML( context.move.description @@ -130,13 +139,30 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati } static async editItem(_, target) { - const actionId = target.dataset.id; - const action = this.move.actions.get(actionId); - await new DHActionConfig(action, async updatedMove => { - await this.settings.updateSource({ [`${this.actionsPath}.${actionId}`]: updatedMove }); + const { type, id } = target.dataset; + if (type === 'effect') { + const effectIndex = this.move.effects.findIndex(x => x.id === id); + const effect = this.move.effects[effectIndex]; + const updatedEffect = + await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect); + if (!updatedEffect) return; + + await this.settings.updateSource({ + [`${this.movePath}.effects`]: this.move.effects.reduce((acc, effect, index) => { + acc.push(index === effectIndex ? { ...updatedEffect, id: effect.id } : effect); + return acc; + }, []) + }); this.move = foundry.utils.getProperty(this.settings, this.movePath); this.render(); - }).render(true); + } else { + const action = this.move.actions.get(id); + await new DHActionConfig(action, async updatedMove => { + await this.settings.updateSource({ [`${this.actionsPath}.${id}`]: updatedMove }); + this.move = foundry.utils.getProperty(this.settings, this.movePath); + this.render(); + }).render(true); + } } static async removeItem(_, target) { @@ -145,16 +171,38 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati this.render(); } + static async addEffect(_, target) { + const currentEffects = foundry.utils.getProperty(this.settings, `${this.movePath}.effects`); + await this.settings.updateSource({ + [`${this.movePath}.effects`]: [ + ...currentEffects, + game.system.api.data.activeEffects.BaseEffect.getDefaultObject() + ] + }); + + this.move = foundry.utils.getProperty(this.settings, this.movePath); + this.render(); + } + static resetMoves() {} + _filterTabs(tabs) { + return this.hasEffects + ? tabs + : Object.keys(tabs).reduce((acc, key) => { + if (key !== 'effects') acc[key] = tabs[key]; + return acc; + }, {}); + } + /** @override */ _onClose(options = {}) { if (!options.submitted) this.move = null; } - static async configure(move, movePath, settings, options = {}) { + static async configure(move, movePath, settings, optionalParts, options = {}) { return new Promise(resolve => { - const app = new this(move, movePath, settings, options); + const app = new this(move, movePath, settings, optionalParts, options); app.addEventListener('close', () => resolve(app.move), { once: true }); app.render({ force: true }); }); diff --git a/module/applications/sheets/items/armor.mjs b/module/applications/sheets/items/armor.mjs index bdc482c3..2550b415 100644 --- a/module/applications/sheets/items/armor.mjs +++ b/module/applications/sheets/items/armor.mjs @@ -8,7 +8,7 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) { tagifyConfigs: [ { selector: '.features-input', - options: () => CONFIG.DH.ITEM.armorFeatures, + options: () => CONFIG.DH.ITEM.orderedArmorFeatures(), callback: ArmorSheet.#onFeatureSelect } ] diff --git a/module/applications/sheets/items/weapon.mjs b/module/applications/sheets/items/weapon.mjs index 2533287b..f5c7dddf 100644 --- a/module/applications/sheets/items/weapon.mjs +++ b/module/applications/sheets/items/weapon.mjs @@ -8,7 +8,7 @@ export default class WeaponSheet extends ItemAttachmentSheet(DHBaseItemSheet) { tagifyConfigs: [ { selector: '.features-input', - options: () => CONFIG.DH.ITEM.weaponFeatures, + options: () => CONFIG.DH.ITEM.orderedWeaponFeatures(), callback: WeaponSheet.#onFeatureSelect } ] diff --git a/module/config/itemConfig.mjs b/module/config/itemConfig.mjs index 7e9f75ea..d815181b 100644 --- a/module/config/itemConfig.mjs +++ b/module/config/itemConfig.mjs @@ -452,6 +452,34 @@ export const armorFeatures = { } }; +export const allArmorFeatures = () => { + const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures + .armorFeatures; + return { + ...armorFeatures, + ...Object.keys(homebrewFeatures).reduce((acc, key) => { + const feature = homebrewFeatures[key]; + acc[key] = { ...feature, label: feature.name }; + return acc; + }, {}) + }; +}; + +export const orderedArmorFeatures = () => { + const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures + .armorFeatures; + const allFeatures = { ...armorFeatures, ...homebrewFeatures }; + const all = Object.keys(allFeatures).map(key => { + const feature = allFeatures[key]; + return { + ...feature, + id: key, + label: feature.label ?? feature.name + }; + }); + return Object.values(all).sort((a, b) => game.i18n.localize(a.label).localeCompare(game.i18n.localize(b.label))); +}; + export const weaponFeatures = { barrier: { label: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.name', @@ -1383,6 +1411,34 @@ export const weaponFeatures = { } }; +export const allWeaponFeatures = () => { + const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures + .weaponFeatures; + return { + ...weaponFeatures, + ...Object.keys(homebrewFeatures).reduce((acc, key) => { + const feature = homebrewFeatures[key]; + acc[key] = { ...feature, label: feature.name }; + return acc; + }, {}) + }; +}; + +export const orderedWeaponFeatures = () => { + const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures + .weaponFeatures; + const allFeatures = { ...weaponFeatures, ...homebrewFeatures }; + const all = Object.keys(allFeatures).map(key => { + const feature = allFeatures[key]; + return { + ...feature, + id: key, + label: feature.label ?? feature.name + }; + }); + return Object.values(all).sort((a, b) => game.i18n.localize(a.label).localeCompare(game.i18n.localize(b.label))); +}; + export const featureTypes = { ancestry: { id: 'ancestry', diff --git a/module/data/activeEffect/baseEffect.mjs b/module/data/activeEffect/baseEffect.mjs index 0ac87de0..770e3462 100644 --- a/module/data/activeEffect/baseEffect.mjs +++ b/module/data/activeEffect/baseEffect.mjs @@ -30,4 +30,24 @@ export default class BaseEffect extends foundry.abstract.TypeDataModel { }) }; } + + static getDefaultObject() { + return { + name: 'New Effect', + id: foundry.utils.randomID(), + disabled: false, + img: 'icons/magic/life/heart-cross-blue.webp', + description: '', + statuses: [], + changes: [], + system: { + rangeDependence: { + enabled: false, + type: CONFIG.DH.GENERAL.rangeInclusion.withinRange.id, + target: CONFIG.DH.GENERAL.otherTargetTypes.hostile.id, + range: CONFIG.DH.GENERAL.range.melee.id + } + } + }; + } } diff --git a/module/data/item/armor.mjs b/module/data/item/armor.mjs index 7f70d3f7..ca1ca004 100644 --- a/module/data/item/armor.mjs +++ b/module/data/item/armor.mjs @@ -1,5 +1,4 @@ import AttachableItem from './attachableItem.mjs'; -import { armorFeatures } from '../../config/itemConfig.mjs'; export default class DHArmor extends AttachableItem { /** @inheritDoc */ @@ -25,7 +24,7 @@ export default class DHArmor extends AttachableItem { new fields.SchemaField({ value: new fields.StringField({ required: true, - choices: CONFIG.DH.ITEM.armorFeatures, + choices: CONFIG.DH.ITEM.allArmorFeatures, blank: true }), effectIds: new fields.ArrayField(new fields.StringField({ required: true })), @@ -60,13 +59,14 @@ export default class DHArmor extends AttachableItem { const allowed = await super._preUpdate(changes, options, user); if (allowed === false) return false; + const changedArmorFeatures = changes.system?.armorFeatures ?? []; + const removedFeatures = this.armorFeatures.filter(x => changedArmorFeatures.every(y => y.value !== x.value)); if (changes.system?.armorFeatures) { - const removed = this.armorFeatures.filter(x => !changes.system.armorFeatures.includes(x)); - const added = changes.system.armorFeatures.filter(x => !this.armorFeatures.includes(x)); + const added = changedArmorFeatures.filter(x => this.armorFeatures.every(y => y.value !== x.value)); const effectIds = []; const actionIds = []; - for (var feature of removed) { + for (var feature of removedFeatures) { effectIds.push(...feature.effectIds); actionIds.push(...feature.actionIds); } @@ -76,8 +76,9 @@ export default class DHArmor extends AttachableItem { return acc; }, {}); + const allFeatures = CONFIG.DH.ITEM.allArmorFeatures(); for (const feature of added) { - const featureData = armorFeatures[feature.value]; + const featureData = allFeatures[feature.value]; if (featureData.effects?.length > 0) { const embeddedItems = await this.parent.createEmbeddedDocuments( 'ActiveEffect', @@ -91,7 +92,7 @@ export default class DHArmor extends AttachableItem { } const newActions = {}; - if (featureData.actions?.length > 0) { + if (featureData.actions?.length > 0 || featureData.actions?.size > 0) { for (let action of featureData.actions) { const embeddedEffects = await this.parent.createEmbeddedDocuments( 'ActiveEffect', @@ -110,10 +111,12 @@ export default class DHArmor extends AttachableItem { { ...cls.getSourceConfig(this), ...action, + type: action.type, _id: actionId, name: game.i18n.localize(action.name), description: game.i18n.localize(action.description), - effects: embeddedEffects.map(x => ({ _id: x.id })) + effects: embeddedEffects.map(x => ({ _id: x.id })), + systemPath: 'actions' }, { parent: this } ); @@ -126,6 +129,10 @@ export default class DHArmor extends AttachableItem { } } + _onUpdate(a, b, c) { + super._onUpdate(a, b, c); + } + /** * Generates a list of localized tags based on this item's type-specific properties. * @returns {string[]} An array of localized tag strings. @@ -145,7 +152,8 @@ export default class DHArmor extends AttachableItem { */ _getLabels() { const labels = []; - if(this.baseScore) labels.push(`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.baseScore}`) + if (this.baseScore) + labels.push(`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.baseScore}`); return labels; } diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 07a5c028..b2d937b5 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -39,7 +39,7 @@ export default class DHWeapon extends AttachableItem { new fields.SchemaField({ value: new fields.StringField({ required: true, - choices: CONFIG.DH.ITEM.weaponFeatures, + choices: CONFIG.DH.ITEM.allWeaponFeatures, blank: true }), effectIds: new fields.ArrayField(new fields.StringField({ required: true })), @@ -116,13 +116,14 @@ export default class DHWeapon extends AttachableItem { const allowed = await super._preUpdate(changes, options, user); if (allowed === false) return false; + const changedWeaponFeatures = changes.system?.weaponFeatures ?? []; + const removedFeatures = this.weaponFeatures.filter(x => changedWeaponFeatures.every(y => y.value !== x.value)); if (changes.system?.weaponFeatures) { - const removed = this.weaponFeatures.filter(x => !changes.system.weaponFeatures.includes(x)); - const added = changes.system.weaponFeatures.filter(x => !this.weaponFeatures.includes(x)); + const added = changedWeaponFeatures.filter(x => this.weaponFeatures.every(y => y.value !== x.value)); const removedEffectsUpdate = []; const removedActionsUpdate = []; - for (let weaponFeature of removed) { + for (let weaponFeature of removedFeatures) { removedEffectsUpdate.push(...weaponFeature.effectIds); removedActionsUpdate.push(...weaponFeature.actionIds); } @@ -133,8 +134,9 @@ export default class DHWeapon extends AttachableItem { return acc; }, {}); + const allFeatures = CONFIG.DH.ITEM.allWeaponFeatures(); for (let weaponFeature of added) { - const featureData = CONFIG.DH.ITEM.weaponFeatures[weaponFeature.value]; + const featureData = allFeatures[weaponFeature.value]; if (featureData.effects?.length > 0) { const embeddedItems = await this.parent.createEmbeddedDocuments( 'ActiveEffect', @@ -148,7 +150,7 @@ export default class DHWeapon extends AttachableItem { } const newActions = {}; - if (featureData.actions?.length > 0) { + if (featureData.actions?.length > 0 || featureData.actions?.size > 0) { for (let action of featureData.actions) { const embeddedEffects = await this.parent.createEmbeddedDocuments( 'ActiveEffect', @@ -170,10 +172,12 @@ export default class DHWeapon extends AttachableItem { { ...cls.getSourceConfig(this), ...action, + type: action.type, _id: actionId, name: game.i18n.localize(action.name), description: game.i18n.localize(action.description), - effects: embeddedEffects.map(x => ({ _id: x.id })) + effects: embeddedEffects.map(x => ({ _id: x.id })), + systemPath: 'actions' }, { parent: this } ); diff --git a/module/data/settings/Homebrew.mjs b/module/data/settings/Homebrew.mjs index 0719b085..ca44a3ed 100644 --- a/module/data/settings/Homebrew.mjs +++ b/module/data/settings/Homebrew.mjs @@ -115,7 +115,35 @@ export default class DhHomebrew extends foundry.abstract.DataModel { label: new fields.StringField({ required: true, label: 'DAGGERHEART.GENERAL.label' }), description: new fields.StringField() }) - ) + ), + itemFeatures: new fields.SchemaField({ + weaponFeatures: new fields.TypedObjectField( + new fields.SchemaField({ + name: new fields.StringField({ required: true }), + img: new fields.FilePathField({ + initial: 'icons/magic/life/cross-worn-green.webp', + categories: ['IMAGE'], + base64: false + }), + description: new fields.HTMLField(), + actions: new ActionsField(), + effects: new fields.ArrayField(new fields.ObjectField()) + }) + ), + armorFeatures: new fields.TypedObjectField( + new fields.SchemaField({ + name: new fields.StringField({ required: true }), + img: new fields.FilePathField({ + initial: 'icons/magic/life/cross-worn-green.webp', + categories: ['IMAGE'], + base64: false + }), + description: new fields.HTMLField(), + actions: new ActionsField(), + effects: new fields.ArrayField(new fields.ObjectField()) + }) + ) + }) }; } } diff --git a/templates/settings/downtime-config/effects.hbs b/templates/settings/downtime-config/effects.hbs new file mode 100644 index 00000000..f09fdb05 --- /dev/null +++ b/templates/settings/downtime-config/effects.hbs @@ -0,0 +1,15 @@ +
+
+ {{localize "DAGGERHEART.GENERAL.Effect.plural"}} + +
+ {{#each move.effects}} + {{> "systems/daggerheart/templates/settings/components/settings-item-line.hbs" id=this.id type="effect" }} + {{/each}} +
+
+
\ No newline at end of file diff --git a/templates/settings/downtime-config/main.hbs b/templates/settings/downtime-config/main.hbs index f8a972c7..7f681368 100644 --- a/templates/settings/downtime-config/main.hbs +++ b/templates/settings/downtime-config/main.hbs @@ -3,11 +3,13 @@ data-tab='{{tabs.main.id}}' data-group='{{tabs.main.group}}' > -
- {{localize "Icon"}} + {{#if hasIcon}} +
+ {{localize "Icon"}} - -
+ +
+ {{/if}}
{{localize "Description"}} diff --git a/templates/settings/homebrew-settings/itemFeatures.hbs b/templates/settings/homebrew-settings/itemFeatures.hbs new file mode 100644 index 00000000..1f8595de --- /dev/null +++ b/templates/settings/homebrew-settings/itemFeatures.hbs @@ -0,0 +1,35 @@ +
+
+
+ + {{localize "DAGGERHEART.GENERAL.weaponFeatures"}} + + + + +
+ {{#each settingFields._source.itemFeatures.weaponFeatures as |feature id|}} + {{> "systems/daggerheart/templates/settings/components/settings-item-line.hbs" this type="weaponFeatures" id=id }} + {{/each}} +
+
+ +
+ + {{localize "DAGGERHEART.GENERAL.armorFeatures"}} + + + + +
+ {{#each settingFields._source.itemFeatures.armorFeatures as |feature id|}} + {{> "systems/daggerheart/templates/settings/components/settings-item-line.hbs" this type="armorFeatures" id=id }} + {{/each}} +
+
+
+
\ No newline at end of file diff --git a/templates/sheets/activeEffect/changes.hbs b/templates/sheets/activeEffect/changes.hbs index 9cf137f0..75f49e4a 100644 --- a/templates/sheets/activeEffect/changes.hbs +++ b/templates/sheets/activeEffect/changes.hbs @@ -8,24 +8,24 @@
    {{#each source.changes as |change i|}} - {{#with ../fields.changes.element.fields as |changeFields|}} -
  1. -
    - -
    -
    - {{formInput changeFields.mode name=(concat "changes." i ".mode") value=change.mode choices=@root.modes}} -
    -
    - {{formInput changeFields.value name=(concat "changes." i ".value") value=change.value}} -
    -
    - {{formInput changeFields.priority name=(concat "changes." i ".priority") value=change.priority - placeholder=(lookup ../../priorities change.mode)}} -
    -
    -
  2. - {{/with}} + {{#with ../fields.changes.element.fields as |changeFields|}} +
  3. +
    + +
    +
    + {{formInput changeFields.mode name=(concat "changes." i ".mode") value=change.mode choices=@root.modes}} +
    +
    + {{formInput changeFields.value name=(concat "changes." i ".value") value=change.value}} +
    +
    + {{formInput changeFields.priority name=(concat "changes." i ".priority") value=change.priority + placeholder=(lookup ../../priorities change.mode)}} +
    +
    +
  4. + {{/with}} {{/each}}
\ No newline at end of file diff --git a/templates/sheets/activeEffect/settings.hbs b/templates/sheets/activeEffect/settings.hbs index 33e1d1b9..cf98c786 100644 --- a/templates/sheets/activeEffect/settings.hbs +++ b/templates/sheets/activeEffect/settings.hbs @@ -2,10 +2,10 @@
{{localize "DAGGERHEART.ACTIVEEFFECT.Config.rangeDependence.title"}} - {{formGroup document.system.schema.fields.rangeDependence.fields.enabled value=source.system.rangeDependence.enabled localize=true }} - {{formGroup document.system.schema.fields.rangeDependence.fields.type value=source.system.rangeDependence.type localize=true }} - {{formGroup document.system.schema.fields.rangeDependence.fields.target value=source.system.rangeDependence.target localize=true }} - {{formGroup document.system.schema.fields.rangeDependence.fields.range value=source.system.rangeDependence.range localize=true }} + {{formGroup systemFields.rangeDependence.fields.enabled value=source.system.rangeDependence.enabled localize=true }} + {{formGroup systemFields.rangeDependence.fields.type value=source.system.rangeDependence.type localize=true }} + {{formGroup systemFields.rangeDependence.fields.target value=source.system.rangeDependence.target localize=true }} + {{formGroup systemFields.rangeDependence.fields.range value=source.system.rangeDependence.range localize=true }}
From 58f039ce969d5b2c63919b3ff5dca16d785b6c83 Mon Sep 17 00:00:00 2001 From: Chris Ryan <73275196+chrisryan10@users.noreply.github.com> Date: Sun, 7 Sep 2025 09:27:46 +1000 Subject: [PATCH 17/18] Add extra features to the Temple Enricher and fix the mousewheel issues with the Template Manager (#1147) Co-authored-by: Chris Ryan --- module/documents/templateManager.mjs | 16 ++++--- module/enrichers/TemplateEnricher.mjs | 65 +++++++++++++++++++++------ 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/module/documents/templateManager.mjs b/module/documents/templateManager.mjs index c31b1baa..cf15c2e3 100644 --- a/module/documents/templateManager.mjs +++ b/module/documents/templateManager.mjs @@ -57,7 +57,10 @@ export default class DhTemplateManager { * @param {wheel Event} event */ #onMouseWheel(event) { - if (!event.shiftKey) return; + if (!this.#activePreview) { + return; + } + if (!event.shiftKey && !event.ctrlKey) return; event.stopPropagation(); event.preventDefault(); const { moveTime, object } = this.#activePreview; @@ -66,8 +69,10 @@ export default class DhTemplateManager { if (now - (moveTime || 0) <= 16) return; this.#activePreview.moveTime = now; + const multiplier = event.shiftKey ? 0.2 : 0.1; + object.document.updateSource({ - direction: object.document.direction + event.deltaY * 0.2 + direction: object.document.direction + event.deltaY * multiplier }); object.renderFlags.set({ refresh: true }); } @@ -77,12 +82,13 @@ export default class DhTemplateManager { * @param {contextmenu Event} event */ #cancelTemplate(event) { - const { mousemove, mousedown, contextmenu } = this.#activePreview.events; + const { mousemove, mousedown, contextmenu, wheel } = this.#activePreview.events; canvas.templates._onDragLeftCancel(event); canvas.stage.off('mousemove', mousemove); canvas.stage.off('mousedown', mousedown); canvas.app.view.removeEventListener('contextmenu', contextmenu); + canvas.app.view.removeEventListener('wheel', wheel); } /** @@ -91,9 +97,9 @@ export default class DhTemplateManager { */ #confirmTemplate(event) { event.stopPropagation(); + this.#cancelTemplate(event); canvas.scene.createEmbeddedDocuments('MeasuredTemplate', [this.#activePreview.document.toObject()]); - - this.#cancelTemplate(event); + this.#activePreview = undefined; } } diff --git a/module/enrichers/TemplateEnricher.mjs b/module/enrichers/TemplateEnricher.mjs index 35a3e231..15936b29 100644 --- a/module/enrichers/TemplateEnricher.mjs +++ b/module/enrichers/TemplateEnricher.mjs @@ -3,6 +3,8 @@ export default function DhTemplateEnricher(match, _options) { let type = null, range = null, + angle = CONFIG.MeasuredTemplate.defaults.angle, + direction = 0, inline = false; parts.forEach(part => { @@ -16,14 +18,24 @@ export default function DhTemplateEnricher(match, _options) { type = matchedType; break; case 'range': - const matchedRange = Object.values(CONFIG.DH.GENERAL.templateRanges).find( - x => x.id.toLowerCase() === split[1] || x.short === split[1] - ); - range = matchedRange?.id; + if (Number.isNaN(Number(split[1]))) { + const matchedRange = Object.values(CONFIG.DH.GENERAL.templateRanges).find( + x => x.id.toLowerCase() === split[1] || x.short === split[1] + ); + range = matchedRange?.id; + } else { + range = split[1]; + } break; case 'inline': inline = true; break; + case 'angle': + angle = split[1]; + break; + case 'direction': + direction = split[1]; + break; } } }); @@ -32,10 +44,32 @@ export default function DhTemplateEnricher(match, _options) { const label = game.i18n.localize(`DAGGERHEART.CONFIG.TemplateTypes.${type}`); + const rangeDisplay = Number.isNaN(Number(range)) ? game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.name`) : range; + + let angleDisplay = ''; + if (angle != CONFIG.MeasuredTemplate.defaults.angle) { + angleDisplay = 'angle:' + angle; + + } + let directionDisplay = ''; + if (direction != 0) { + directionDisplay = 'direction:' + direction; + } + + let extraDisplay = ''; + if (angleDisplay != '' && directionDisplay != '') { + extraDisplay = ' (' + angleDisplay + '|' + directionDisplay + ')'; + } else if (angleDisplay != '') { + extraDisplay = ' (' + angleDisplay + ')'; + } else if (directionDisplay != '') { + extraDisplay = ' (' + directionDisplay + ')'; + } + const templateElement = document.createElement('span'); templateElement.innerHTML = ` - `; @@ -45,21 +79,25 @@ export default function DhTemplateEnricher(match, _options) { export const renderMeasuredTemplate = async event => { const button = event.currentTarget, type = button.dataset.type, - range = button.dataset.range; + range = button.dataset.range, + angle = button.dataset.angle, + direction = button.dataset.direction; if (!type || !range || !game.canvas.scene) return; const usedType = type === 'inFront' ? 'cone' : type === 'emanation' ? 'circle' : type; - const angle = + const usedAngle = type === CONST.MEASURED_TEMPLATE_TYPES.CONE - ? CONFIG.MeasuredTemplate.defaults.angle + ? (angle ?? CONFIG.MeasuredTemplate.defaults.angle) : type === CONFIG.DH.GENERAL.templateTypes.INFRONT ? '180' : undefined; - const baseDistance = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement[ - range - ]; + let baseDistance = range; + if (Number.isNaN(Number(range))) { + baseDistance = + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement[range]; + } const distance = type === CONFIG.DH.GENERAL.templateTypes.EMANATION ? baseDistance + 2.5 : baseDistance; const { width, height } = game.canvas.scene.dimensions; @@ -69,7 +107,8 @@ export const renderMeasuredTemplate = async event => { t: usedType, distance: distance, width: type === CONST.MEASURED_TEMPLATE_TYPES.RAY ? 5 : undefined, - angle: angle + angle: usedAngle, + direction: direction }; CONFIG.ux.TemplateManager.createPreview(data); From a57d154d45cba43775025896f6beb656845cb7d3 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Tue, 9 Sep 2025 22:03:13 -0400 Subject: [PATCH 18/18] Add space between paragraphs (#1172) --- styles/less/global/elements.less | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index 65e825a9..5d6e97d1 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -210,7 +210,13 @@ } p { - margin: 0; + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } } ul {