From a4fff56461a105bc5389a708f8c53ce4dd3aa115 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Wed, 25 Mar 2026 17:07:20 +0100 Subject: [PATCH 01/19] Fixed base resources getting their values capped to max at prepareDerivedData. Fixed effect autocomplete not having labels for baseResources. --- module/data/actor/character.mjs | 2 -- module/data/actor/creature.mjs | 10 ++++++++++ module/data/fields/actorField.mjs | 11 ++++++----- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 10d50301..cde7d280 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -660,7 +660,6 @@ export default class DhCharacter extends DhCreature { prepareDerivedData() { super.prepareDerivedData(); - let baseHope = this.resources.hope.value; if (this.companion) { for (let levelKey in this.companion.system.levelData.levelups) { const level = this.companion.system.levelData.levelups[levelKey]; @@ -675,7 +674,6 @@ export default class DhCharacter extends DhCreature { } this.resources.hope.max -= this.scars; - this.resources.hope.value = Math.min(baseHope, this.resources.hope.max); this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait; this.resources.armor = { diff --git a/module/data/actor/creature.mjs b/module/data/actor/creature.mjs index 601068ad..88646301 100644 --- a/module/data/actor/creature.mjs +++ b/module/data/actor/creature.mjs @@ -60,4 +60,14 @@ export default class DhCreature extends BaseDataActor { } } } + + prepareDerivedData() { + const minLimitResource = resource => { + if (resource) resource.value = Math.min(resource.value, resource.max); + }; + + minLimitResource(this.resources.stress); + minLimitResource(this.resources.hitPoints); + minLimitResource(this.resources.hope); + } } diff --git a/module/data/fields/actorField.mjs b/module/data/fields/actorField.mjs index 76c24319..a3c17281 100644 --- a/module/data/fields/actorField.mjs +++ b/module/data/fields/actorField.mjs @@ -83,19 +83,20 @@ class ResourcesField extends fields.TypedObjectField { return data; } - /** + /** * Foundry bar attributes are unable to handle finding the schema field nor the label normally. * This returns the element if its a valid resource key and overwrites the element's label for that retrieval. */ _getField(path) { - if ( path.length === 0 ) return this; + if (path.length === 0) return this; const first = path.shift(); if (first === this.element.name) return this.element_getField(path); - + const resources = CONFIG.DH.RESOURCE[this.actorType].all; if (first in resources) { - this.element.label = resources[first].label; - return this.element._getField(path); + const field = this.element._getField(path); + field.label = resources[first].label; + return field; } return undefined; From eb9e47c39d341f25328c51c2d809a1f0f7b11fa1 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Thu, 26 Mar 2026 10:50:52 -0400 Subject: [PATCH 02/19] Refresh effects display after actor preparation (#1752) --- module/applications/ui/effectsDisplay.mjs | 1 - module/documents/actor.mjs | 12 ++++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/module/applications/ui/effectsDisplay.mjs b/module/applications/ui/effectsDisplay.mjs index 8c0c939c..e0fa7ae2 100644 --- a/module/applications/ui/effectsDisplay.mjs +++ b/module/applications/ui/effectsDisplay.mjs @@ -91,7 +91,6 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica const effects = DhEffectsDisplay.getTokenEffects(); const effect = effects.find(x => x.id === element.dataset.effectId); await effect.delete(); - this.render(); } setupHooks() { diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 13843bb8..eea2e212 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -29,6 +29,18 @@ export default class DhpActor extends Actor { return this.system.metadata.isNPC; } + prepareData() { + super.prepareData(); + + // Update effects if it is the user's character or is controlled + if (canvas.ready) { + const controlled = canvas.tokens.controlled.some((t) => t.actor === this); + if (game.user.character === this || controlled) { + ui.effectsDisplay.render(); + } + } + } + /* -------------------------------------------- */ /** @inheritDoc */ From a4adbf8ac4cf90f73af5e4eaed1a4e57b49501de Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:12:05 +0100 Subject: [PATCH 03/19] Added system keybind for spotlighting a combatant (#1749) --- daggerheart.mjs | 2 ++ lang/en.json | 14 ++++++++++++++ module/_module.mjs | 1 + module/config/settingsConfig.mjs | 4 ++++ module/macros/_modules.mjs | 1 + module/macros/spotlightCombatant.mjs | 21 +++++++++++++++++++++ module/systemRegistration/settings.mjs | 20 ++++++++++++++++++++ system.json | 2 +- 8 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 module/macros/_modules.mjs create mode 100644 module/macros/spotlightCombatant.mjs diff --git a/daggerheart.mjs b/daggerheart.mjs index 5960c6b1..abe12524 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -3,6 +3,7 @@ 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 { macros } from './module/_module.mjs'; import * as collections from './module/documents/collections/_module.mjs'; import * as dice from './module/dice/_module.mjs'; import * as fields from './module/data/fields/_module.mjs'; @@ -93,6 +94,7 @@ Hooks.once('init', () => { data, models, documents, + macros, dice, fields }; diff --git a/lang/en.json b/lang/en.json index 4032b6ed..feeb65b2 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2486,6 +2486,14 @@ "secondaryWeapon": "Secondary Weapon" } }, + "MACROS": { + "Spotlight": { + "errors": { + "noActiveCombat": "There is no active encounter", + "noCombatantSelected": "A combatant token must be either selected or hovered to spotlight it" + } + } + }, "ROLLTABLES": { "FIELDS": { "formulaName": { "label": "Formula Name" } @@ -2725,6 +2733,12 @@ "setResourceIdentifier": "Set Resource Identifier" } }, + "Keybindings": { + "spotlight": { + "name": "Spotlight Combatant", + "hint": "Move the spotlight to a hovered or selected token that's present in an active encounter" + } + }, "Menu": { "title": "Daggerheart Game Settings", "automation": { diff --git a/module/_module.mjs b/module/_module.mjs index 2e1d6fb4..4a00e97c 100644 --- a/module/_module.mjs +++ b/module/_module.mjs @@ -7,3 +7,4 @@ export * as documents from './documents/_module.mjs'; export * as enrichers from './enrichers/_module.mjs'; export * as helpers from './helpers/_module.mjs'; export * as systemRegistration from './systemRegistration/_module.mjs'; +export * as macros from './macros/_modules.mjs'; diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index 0b28f0ab..c19e6e26 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -1,3 +1,7 @@ +export const keybindings = { + spotlight: 'DHSpotlight' +}; + export const menu = { Automation: { Name: 'GameSettingsAutomation', diff --git a/module/macros/_modules.mjs b/module/macros/_modules.mjs new file mode 100644 index 00000000..d4a5599f --- /dev/null +++ b/module/macros/_modules.mjs @@ -0,0 +1 @@ +export { default as spotlightCombatant } from './spotlightCombatant.mjs'; diff --git a/module/macros/spotlightCombatant.mjs b/module/macros/spotlightCombatant.mjs new file mode 100644 index 00000000..68a26ff9 --- /dev/null +++ b/module/macros/spotlightCombatant.mjs @@ -0,0 +1,21 @@ +/** + * Spotlights a combatant. + * The combatant can be selected in a number of ways. If many are applied at the same time, the following order is used: + * 1) SelectedCombatant + * 2) HoveredCombatant + */ +const spotlightCombatant = () => { + if (!game.combat) + return ui.notifications.error(game.i18n.localize('DAGGERHEART.MACROS.Spotlight.errors.noActiveCombat')); + + const selectedCombatant = canvas.tokens.controlled.length > 0 ? canvas.tokens.controlled[0].combatant : null; + const hoveredCombatant = game.canvas.tokens.hover?.combatant; + + const combatant = selectedCombatant ?? hoveredCombatant; + if (!combatant) + return ui.notifications.error(game.i18n.localize('DAGGERHEART.MACROS.Spotlight.errors.noCombatantSelected')); + + ui.combat.setCombatantSpotlight(combatant.id); +}; + +export default spotlightCombatant; diff --git a/module/systemRegistration/settings.mjs b/module/systemRegistration/settings.mjs index e7ec37f5..94ad5da7 100644 --- a/module/systemRegistration/settings.mjs +++ b/module/systemRegistration/settings.mjs @@ -18,6 +18,7 @@ import { import { CompendiumBrowserSettings, DhTagTeamRoll } from '../data/_module.mjs'; export const registerDHSettings = () => { + registerKeyBindings(); registerMenuSettings(); registerMenus(); registerNonConfigSettings(); @@ -33,6 +34,25 @@ export const registerDHSettings = () => { }); }; +export const registerKeyBindings = () => { + game.keybindings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.keybindings.spotlight, { + name: game.i18n.localize('DAGGERHEART.SETTINGS.Keybindings.spotlight.name'), + hint: game.i18n.localize('DAGGERHEART.SETTINGS.Keybindings.spotlight.hint'), + uneditable: [], + editable: [ + { + key: 's', + modifiers: [] + } + ], + onDown: game.system.api.macros.spotlightCombatant, + onUp: () => {}, + restricted: true, + reservedModifiers: [], + precedence: CONST.KEYBINDING_PRECEDENCE.NORMAL + }); +}; + const registerMenuSettings = () => { game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules, { scope: 'world', diff --git a/system.json b/system.json index 44c26886..9242a24a 100644 --- a/system.json +++ b/system.json @@ -2,7 +2,7 @@ "id": "daggerheart", "title": "Daggerheart", "description": "An unofficial implementation of the Daggerheart system", - "version": "1.9.5", + "version": "1.9.6", "compatibility": { "minimum": "13.346", "verified": "13.351", From 394d1d338d3cd0e6b6e2445233a28e0d34752f57 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Thu, 26 Mar 2026 16:27:09 +0100 Subject: [PATCH 04/19] Corrected BaseEffect scrollText armorUpdate logic --- module/data/activeEffect/baseEffect.mjs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/module/data/activeEffect/baseEffect.mjs b/module/data/activeEffect/baseEffect.mjs index e3f4137d..bac50c56 100644 --- a/module/data/activeEffect/baseEffect.mjs +++ b/module/data/activeEffect/baseEffect.mjs @@ -161,12 +161,11 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel { this.parent.actor?.type === 'character' && this.parent.actor.system.resources.armor ) { - const newArmorTotal = (changed.system?.changes ?? []).reduce((acc, change) => { - if (change.type === 'armor') acc += change.value.current; - return acc; - }, this.parent.actor.system.armor?.system?.armor?.current ?? 0); + const armorEffect = changed.system?.changes?.find(x => x.type === 'armor'); + const newArmorTotal = + armorEffect?.value?.current + (this.parent.actor.system.armor?.system?.armor?.current ?? 0); - if (newArmorTotal !== this.parent.actor.system.armorScore.value) { + if (armorEffect && newArmorTotal !== this.parent.actor.system.armorScore.value) { const armorData = getScrollTextData(this.parent.actor, { value: newArmorTotal }, 'armor'); options.scrollingTextData = [armorData]; } From 94a2a5723b38015a6d4b8df5f3cc9733eff7bea8 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Thu, 26 Mar 2026 16:34:24 +0100 Subject: [PATCH 05/19] Removed default spotlight keybind key --- module/systemRegistration/settings.mjs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/module/systemRegistration/settings.mjs b/module/systemRegistration/settings.mjs index 94ad5da7..17dab6f7 100644 --- a/module/systemRegistration/settings.mjs +++ b/module/systemRegistration/settings.mjs @@ -39,12 +39,7 @@ export const registerKeyBindings = () => { name: game.i18n.localize('DAGGERHEART.SETTINGS.Keybindings.spotlight.name'), hint: game.i18n.localize('DAGGERHEART.SETTINGS.Keybindings.spotlight.hint'), uneditable: [], - editable: [ - { - key: 's', - modifiers: [] - } - ], + editable: [], onDown: game.system.api.macros.spotlightCombatant, onUp: () => {}, restricted: true, From d7ce388cad1f217bbe438e1474e14e393e3a6fd7 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Fri, 27 Mar 2026 10:29:01 +0100 Subject: [PATCH 06/19] Fixed resource error on TagTeamDialog reroll --- module/applications/dialogs/tagTeamDialog.mjs | 3 ++- module/dice/dhRoll.mjs | 4 ---- module/dice/dualityRoll.mjs | 12 ++++++------ 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/module/applications/dialogs/tagTeamDialog.mjs b/module/applications/dialogs/tagTeamDialog.mjs index 27003162..5fdefcbd 100644 --- a/module/applications/dialogs/tagTeamDialog.mjs +++ b/module/applications/dialogs/tagTeamDialog.mjs @@ -453,7 +453,8 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio const { parsedRoll, newRoll } = await game.system.api.dice.DualityRoll.reroll( memberData.rollData, dieIndex, - diceType + diceType, + false ); const rollData = parsedRoll.toJSON(); this.updatePartyData( diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index a5d95cd1..3310b9ca 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -122,10 +122,6 @@ export default class DHRoll extends Roll { if (roll._evaluated) { const message = await cls.create(msgData, { messageMode: config.selectedMessageMode }); - if (config.tagTeamSelected) { - game.system.api.applications.dialogs.TagTeamDialog.assignRoll(message.speakerActor, message); - } - if (roll.formula !== '' && game.modules.get('dice-so-nice')?.active) { await game.dice3d.waitFor3DAnimationByMessageID(message.id); } diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index 03035f68..84e0b493 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -305,7 +305,6 @@ export default class DualityRoll extends D20Roll { !config.source?.actor || (game.user.isGM ? !hopeFearAutomation.gm : !hopeFearAutomation.players) || config.actionType === 'reaction' || - config.tagTeamSelected || config.skips?.resources ) return; @@ -346,7 +345,6 @@ export default class DualityRoll extends D20Roll { if ( automationSettings.countdownAutomation && config.actionType !== 'reaction' && - !config.tagTeamSelected && !config.skips?.updateCountdowns ) { const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns; @@ -374,7 +372,7 @@ export default class DualityRoll extends D20Roll { } } - static async reroll(rollBase, dieIndex, diceType) { + static async reroll(rollBase, dieIndex, diceType, updateResources = true) { let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollBase, evaluated: false }); const term = parsedRoll.terms[dieIndex]; await term.reroll(`/r1=${term.total}`); @@ -421,12 +419,14 @@ export default class DualityRoll extends D20Roll { source: { actor: parsedRoll.options.source.actor ?? '' }, targets: parsedRoll.targets, roll: newRoll, - rerolledRoll: parsedRoll.roll, + rerolledRoll: parsedRoll.options.roll, resourceUpdates: new ResourceUpdateMap(actor) }; - await DualityRoll.addDualityResourceUpdates(config); - await config.resourceUpdates.updateResources(); + if (updateResources) { + await DualityRoll.addDualityResourceUpdates(config); + await config.resourceUpdates.updateResources(); + } return { newRoll, parsedRoll }; } From d79c236cfefb470935188b9d20e1ea92b2d5e6c5 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Fri, 27 Mar 2026 22:11:01 +0100 Subject: [PATCH 07/19] Fixed effects not being creatable when not on an actor --- module/documents/activeEffect.mjs | 60 ++++++++++++++++--------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/module/documents/activeEffect.mjs b/module/documents/activeEffect.mjs index 4aeba3af..959971d5 100644 --- a/module/documents/activeEffect.mjs +++ b/module/documents/activeEffect.mjs @@ -108,37 +108,41 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { update.img = 'icons/magic/life/heart-cross-blue.webp'; } - const existingEffect = this.actor.effects.find(x => x.origin === data.origin); - const stacks = Boolean(data.system?.stacking); - if (existingEffect && !stacks) return false; + if (this.actor) { + const existingEffect = this.actor.effects.find(x => x.origin === data.origin); + const stacks = Boolean(data.system?.stacking); + if (existingEffect && !stacks) return false; - if (existingEffect && stacks) { - const incrementedValue = existingEffect.system.stacking.value + 1; - await existingEffect.update({ - 'system.stacking.value': Math.min(incrementedValue, existingEffect.system.stacking.max ?? Infinity) - }); - return false; + if (existingEffect && stacks) { + const incrementedValue = existingEffect.system.stacking.value + 1; + await existingEffect.update({ + 'system.stacking.value': Math.min(incrementedValue, existingEffect.system.stacking.max ?? Infinity) + }); + return false; + } } - const statuses = Object.keys(data.statuses ?? {}); - const immuneStatuses = - statuses.filter( - status => - this.parent.system.rules?.conditionImmunities && - this.parent.system.rules.conditionImmunities[status] - ) ?? []; - if (immuneStatuses.length > 0) { - update.statuses = statuses.filter(x => !immuneStatuses.includes(x)); - const conditions = CONFIG.DH.GENERAL.conditions(); - const scrollingTexts = immuneStatuses.map(status => ({ - text: game.i18n.format('DAGGERHEART.ACTIVEEFFECT.immuneStatusText', { - status: game.i18n.localize(conditions[status].name) - }) - })); - if (update.statuses.length > 0) { - setTimeout(() => scrollingTexts, 500); - } else { - this.parent.queueScrollText(scrollingTexts); + if (this.parent) { + const statuses = Object.keys(data.statuses ?? {}); + const immuneStatuses = + statuses.filter( + status => + this.parent.system.rules?.conditionImmunities && + this.parent.system.rules.conditionImmunities[status] + ) ?? []; + if (immuneStatuses.length > 0) { + update.statuses = statuses.filter(x => !immuneStatuses.includes(x)); + const conditions = CONFIG.DH.GENERAL.conditions(); + const scrollingTexts = immuneStatuses.map(status => ({ + text: game.i18n.format('DAGGERHEART.ACTIVEEFFECT.immuneStatusText', { + status: game.i18n.localize(conditions[status].name) + }) + })); + if (update.statuses.length > 0) { + setTimeout(() => scrollingTexts, 500); + } else { + this.parent.queueScrollText(scrollingTexts); + } } } From 7f8e3fee6e2c0ac3035502354a6431846cb939cd Mon Sep 17 00:00:00 2001 From: WBHarry Date: Fri, 27 Mar 2026 22:24:13 +0100 Subject: [PATCH 08/19] Fixed ActiveEffect preCreate blocking multiple effects with origin=null --- module/documents/activeEffect.mjs | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/module/documents/activeEffect.mjs b/module/documents/activeEffect.mjs index 959971d5..8ec7a751 100644 --- a/module/documents/activeEffect.mjs +++ b/module/documents/activeEffect.mjs @@ -1,5 +1,4 @@ import { itemAbleRollParse } from '../helpers/utils.mjs'; -import { RefreshType } from '../systemRegistration/socket.mjs'; export default class DhActiveEffect extends foundry.documents.ActiveEffect { /* -------------------------------------------- */ @@ -108,7 +107,7 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { update.img = 'icons/magic/life/heart-cross-blue.webp'; } - if (this.actor) { + if (this.actor && data.origin) { const existingEffect = this.actor.effects.find(x => x.origin === data.origin); const stacks = Boolean(data.system?.stacking); if (existingEffect && !stacks) return false; @@ -153,20 +152,6 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { await super._preCreate(data, options, user); } - /** @inheritdoc */ - _onCreate(data, options, userId) { - super._onCreate(data, options, userId); - - Hooks.callAll(RefreshType.EffectsDisplay); - } - - /** @inheritdoc */ - _onDelete(data, options, userId) { - super._onDelete(data, options, userId); - - Hooks.callAll(RefreshType.EffectsDisplay); - } - /* -------------------------------------------- */ /* Methods */ /* -------------------------------------------- */ From e3c4f1ce9fbbe55d7f88808b6a45be206e70dec3 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Sat, 28 Mar 2026 00:17:53 +0100 Subject: [PATCH 09/19] Raised version to Testing 3 --- system.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system.json b/system.json index 9a78bf50..cc322576 100644 --- a/system.json +++ b/system.json @@ -5,7 +5,7 @@ "version": "2.0.0", "compatibility": { "minimum": "14.355", - "verified": "14.357", + "verified": "14.358", "maximum": "14" }, "authors": [ From c730cc3d4d15e13d9c1a900fe8a56bcf781ab034 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sat, 28 Mar 2026 00:18:35 +0100 Subject: [PATCH 10/19] [Feature] 1740 - Beastform Info (#1750) * Improved the EffectDisplay tooltip of the beastform effect to show the info about the active beastform * . * Move template to more sorted location --------- Co-authored-by: Carlos Fernandez --- module/data/activeEffect/beastformEffect.mjs | 2 +- module/data/item/beastform.mjs | 10 +- module/documents/tooltipManager.mjs | 33 ++++- module/systemRegistration/handlebars.mjs | 1 + .../less/sheets/actors/character/sidebar.less | 9 +- styles/less/ux/index.less | 1 + styles/less/ux/tooltip/bordered-tooltip.less | 2 +- styles/less/ux/tooltip/sheet.less | 129 ++++++++++++++++++ styles/less/ux/tooltip/tooltip.less | 111 --------------- templates/ui/tooltip/beastform.hbs | 32 +---- templates/ui/tooltip/effect-display.hbs | 28 ++-- templates/ui/tooltip/parts/beastformData.hbs | 31 +++++ templates/ui/tooltip/parts/tooltipTags.hbs | 1 + 13 files changed, 226 insertions(+), 164 deletions(-) create mode 100644 styles/less/ux/tooltip/sheet.less create mode 100644 templates/ui/tooltip/parts/beastformData.hbs diff --git a/module/data/activeEffect/beastformEffect.mjs b/module/data/activeEffect/beastformEffect.mjs index 47e28b4c..128c0c52 100644 --- a/module/data/activeEffect/beastformEffect.mjs +++ b/module/data/activeEffect/beastformEffect.mjs @@ -25,7 +25,7 @@ export default class BeastformEffect extends BaseEffect { width: new fields.NumberField({ integer: false, nullable: true }) }) }), - advantageOn: new fields.ArrayField(new fields.StringField()), + advantageOn: new fields.TypedObjectField(new fields.SchemaField({ value: new fields.StringField() })), featureIds: new fields.ArrayField(new fields.StringField()), effectIds: new fields.ArrayField(new fields.StringField()) }; diff --git a/module/data/item/beastform.mjs b/module/data/item/beastform.mjs index 2792f7e3..d30d6046 100644 --- a/module/data/item/beastform.mjs +++ b/module/data/item/beastform.mjs @@ -99,10 +99,14 @@ export default class DHBeastform extends BaseDataItem { get beastformAttackData() { const effect = this.parent.effects.find(x => x.type === 'beastform'); + return DHBeastform.getBeastformAttackData(effect); + } + + static getBeastformAttackData(effect) { if (!effect) return null; - const traitBonus = - effect.system.changes.find(x => x.key === `system.traits.${this.mainTrait}.value`)?.value ?? 0; + const mainTrait = effect.system.changes.find(x => x.key === 'system.rules.attack.roll.trait')?.value; + const traitBonus = effect.system.changes.find(x => x.key === `system.traits.${mainTrait}.value`)?.value ?? 0; const evasionBonus = effect.system.changes.find(x => x.key === 'system.evasion')?.value ?? 0; const damageDiceIndex = effect.system.changes.find(x => x.key === 'system.rules.attack.damage.diceIndex'); @@ -110,7 +114,7 @@ export default class DHBeastform extends BaseDataItem { const damageBonus = effect.system.changes.find(x => x.key === 'system.rules.attack.damage.bonus')?.value ?? 0; return { - trait: game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.mainTrait].label), + trait: game.i18n.localize(CONFIG.DH.ACTOR.abilities[mainTrait]?.label), traitBonus: traitBonus ? Number(traitBonus).signedString() : '', evasionBonus: evasionBonus ? Number(evasionBonus).signedString() : '', damageDice: damageDice, diff --git a/module/documents/tooltipManager.mjs b/module/documents/tooltipManager.mjs index bf107a42..e10dc5fa 100644 --- a/module/documents/tooltipManager.mjs +++ b/module/documents/tooltipManager.mjs @@ -31,12 +31,39 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti this.#bordered = true; let effect = {}; if (element.dataset.uuid) { - const effectData = (await foundry.utils.fromUuid(element.dataset.uuid)).toObject(); + const effectItem = await foundry.utils.fromUuid(element.dataset.uuid); + const effectData = effectItem.toObject(); + effect = { ...effectData, - name: game.i18n.localize(effectData.name), - description: game.i18n.localize(effectData.description ?? effectData.parent.system.description) + name: game.i18n.localize(effectData.name) }; + + if (effectData.type === 'beastform') { + const beastformData = { + features: [], + advantageOn: effectData.system.advantageOn, + beastformAttackData: game.system.api.data.items.DHBeastform.getBeastformAttackData(effectItem) + }; + + const features = effectItem.parent.items.filter(x => effectItem.system.featureIds.includes(x.id)); + for (const feature of features) { + const featureData = feature.toObject(); + featureData.enrichedDescription = await feature.system.getEnrichedDescription(); + beastformData.features.push(featureData); + } + + effect.description = await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/ui/tooltip/parts/beastformData.hbs', + { + item: { system: beastformData } + } + ); + } else { + effect.description = game.i18n.localize( + effectData.description ?? effectData.parent.system.description + ); + } } else { const conditions = CONFIG.DH.GENERAL.conditions(); const condition = conditions[element.dataset.condition]; diff --git a/module/systemRegistration/handlebars.mjs b/module/systemRegistration/handlebars.mjs index 36df8b54..63e591c6 100644 --- a/module/systemRegistration/handlebars.mjs +++ b/module/systemRegistration/handlebars.mjs @@ -36,6 +36,7 @@ export const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/actionTypes/summon.hbs', 'systems/daggerheart/templates/actionTypes/transform.hbs', 'systems/daggerheart/templates/settings/components/settings-item-line.hbs', + 'systems/daggerheart/templates/ui/tooltip/parts/beastformData.hbs', 'systems/daggerheart/templates/ui/tooltip/parts/tooltipChips.hbs', 'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs', 'systems/daggerheart/templates/dialogs/downtime/activities.hbs', diff --git a/styles/less/sheets/actors/character/sidebar.less b/styles/less/sheets/actors/character/sidebar.less index bb0a43cd..8bbede76 100644 --- a/styles/less/sheets/actors/character/sidebar.less +++ b/styles/less/sheets/actors/character/sidebar.less @@ -339,7 +339,8 @@ background: light-dark(@light-black, @dark-blue); border: 1px solid light-dark(@dark-blue, @golden); - h4, i { + h4, + i { color: light-dark(@dark-blue, @golden); } } @@ -380,7 +381,7 @@ } } } - .slot-label { + .slot-label { display: flex; align-items: center; color: light-dark(@beige, @dark-blue); @@ -399,7 +400,9 @@ background: light-dark(@light-black, @dark-blue); border: 1px solid light-dark(@dark-blue, @golden); - .label, .value, i { + .label, + .value, + i { color: light-dark(@dark-blue, @golden); } } diff --git a/styles/less/ux/index.less b/styles/less/ux/index.less index 6fad3ab1..a73f2d1c 100644 --- a/styles/less/ux/index.less +++ b/styles/less/ux/index.less @@ -1,3 +1,4 @@ +@import './tooltip/sheet.less'; @import './tooltip/tooltip.less'; @import './tooltip/armorManagement.less'; @import './tooltip/battlepoints.less'; diff --git a/styles/less/ux/tooltip/bordered-tooltip.less b/styles/less/ux/tooltip/bordered-tooltip.less index abec93b7..d72f635e 100644 --- a/styles/less/ux/tooltip/bordered-tooltip.less +++ b/styles/less/ux/tooltip/bordered-tooltip.less @@ -18,7 +18,7 @@ display: flex; flex-direction: column; align-items: center; - text-align: start; + text-align: center; padding: 5px; gap: 0px; diff --git a/styles/less/ux/tooltip/sheet.less b/styles/less/ux/tooltip/sheet.less new file mode 100644 index 00000000..59e4e638 --- /dev/null +++ b/styles/less/ux/tooltip/sheet.less @@ -0,0 +1,129 @@ +#tooltip:has(div.daggerheart.dh-style.tooltip.card-style), +aside[role='tooltip']:has(div.daggerheart.dh-style.tooltip), +#tooltip.bordered-tooltip { + .tooltip-title { + font-size: var(--font-size-20); + color: light-dark(@dark-blue, @golden); + font-weight: 700; + } + + .tooltip-description { + font-style: inherit; + text-align: inherit; + width: 100%; + padding: 5px 10px; + position: relative; + margin-top: 5px; + + &::before { + content: ''; + background: @golden; + mask-image: linear-gradient(270deg, transparent 0%, black 50%, transparent 100%); + height: 2px; + width: calc(100% - 10px); + } + + &::before { + position: absolute; + top: -5px; + } + } + + .tooltip-separator { + background: @golden; + mask-image: linear-gradient(270deg, transparent 0%, black 50%, transparent 100%); + height: 2px; + width: calc(100% - 10px); + margin-bottom: 2px; + } + + .tooltip-tags { + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; + padding: 5px 10px; + position: relative; + max-height: 150px; + overflow-y: auto; + position: relative; + + scrollbar-width: thin; + scrollbar-color: light-dark(@dark-blue, @golden) transparent; + + .tooltip-tag { + display: flex; + gap: 10px; + flex-direction: column; + + .tooltip-tag-label-container { + display: flex; + align-items: center; + gap: 5px; + + img { + width: 40px; + height: 40px; + border-radius: 3px; + } + } + } + } + + .tags { + display: flex; + gap: 5px 10px; + padding-bottom: 16px; + flex-wrap: wrap; + justify-content: center; + + &.advantages { + width: 100%; + padding: 5px 10px; + padding-bottom: 16px; + position: relative; + margin-top: 5px; + + &::before { + content: ''; + background: @golden; + mask-image: linear-gradient(270deg, transparent 0%, black 50%, transparent 100%); + height: 2px; + width: calc(100% - 10px); + } + + &::before { + position: absolute; + top: -5px; + } + + .tag { + background: @green-10; + color: @green; + border-color: @green; + } + } + + .tag { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 3px 5px; + font-size: var(--font-size-12); + font: @font-body; + + background: light-dark(@dark-15, @beige-15); + border: 1px solid light-dark(@dark, @beige); + border-radius: 3px; + } + + .label { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + font-size: var(--font-size-12); + } + } +} diff --git a/styles/less/ux/tooltip/tooltip.less b/styles/less/ux/tooltip/tooltip.less index ac09afc0..1566059f 100644 --- a/styles/less/ux/tooltip/tooltip.less +++ b/styles/less/ux/tooltip/tooltip.less @@ -13,13 +13,6 @@ aside[role='tooltip']:has(div.daggerheart.dh-style.tooltip.card-style) { outline: 1px solid light-dark(@dark-80, @beige-80); box-shadow: 0 0 25px rgba(0, 0, 0, 0.8); - .tooltip-title { - font-size: var(--font-size-20); - color: light-dark(@dark-blue, @golden); - font-weight: 700; - margin-bottom: 5px; - } - .tooltip-subtitle { margin: 0; } @@ -80,110 +73,6 @@ aside[role='tooltip']:has(div.daggerheart.dh-style.tooltip.card-style) { } } - .tooltip-tags { - display: flex; - flex-direction: column; - gap: 10px; - width: 100%; - padding: 5px 10px; - position: relative; - padding-top: 10px; - max-height: 150px; - overflow-y: auto; - position: relative; - - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; - - &::before { - content: ''; - background: @golden; - mask-image: linear-gradient(270deg, transparent 0%, black 50%, transparent 100%); - height: 2px; - width: calc(100% - 10px); - } - - &::before { - position: absolute; - top: 0px; - } - - .tooltip-tag { - display: flex; - gap: 10px; - flex-direction: column; - - .tooltip-tag-label-container { - display: flex; - align-items: center; - gap: 5px; - - img { - width: 40px; - height: 40px; - border-radius: 3px; - } - } - } - } - - .tags { - display: flex; - gap: 5px 10px; - padding-bottom: 16px; - flex-wrap: wrap; - justify-content: center; - - &.advantages { - width: 100%; - padding: 5px 10px; - padding-bottom: 16px; - position: relative; - margin-top: 5px; - - &::before { - content: ''; - background: @golden; - mask-image: linear-gradient(270deg, transparent 0%, black 50%, transparent 100%); - height: 2px; - width: calc(100% - 10px); - } - - &::before { - position: absolute; - top: -5px; - } - - .tag { - background: @green-10; - color: @green; - border-color: @green; - } - } - - .tag { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - padding: 3px 5px; - font-size: var(--font-size-12); - font: @font-body; - - background: light-dark(@dark-15, @beige-15); - border: 1px solid light-dark(@dark, @beige); - border-radius: 3px; - } - - .label { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - font-size: var(--font-size-12); - } - } - .item-icons-list { position: absolute; display: flex; diff --git a/templates/ui/tooltip/beastform.hbs b/templates/ui/tooltip/beastform.hbs index 1b04ac82..3d07f78f 100644 --- a/templates/ui/tooltip/beastform.hbs +++ b/templates/ui/tooltip/beastform.hbs @@ -3,37 +3,7 @@

{{item.name}}

{{item.system.examples}}

- {{#if description}} -
{{{description}}}
- {{/if}} - -
- {{#with item.system.beastformAttackData}} -
- {{localize "DAGGERHEART.ITEMS.Beastform.mainTrait"}} {{this.trait}} -
-
- {{localize "DAGGERHEART.ITEMS.Beastform.traitBonus"}} {{this.traitBonus}} -
-
- {{localize "DAGGERHEART.GENERAL.evasion"}} {{this.evasionBonus}} -
-
- {{localize "DAGGERHEART.GENERAL.damage"}} {{concat this.damageDice ' ' this.damageBonus}} -
- {{/with}} -
- -

{{localize "DAGGERHEART.ITEMS.Beastform.FIELDS.advantageOn.label"}}

-
- {{#each item.system.advantageOn as | chip |}} -
- {{ifThen chip.value chip.value chip}} -
- {{/each}} -
- - {{> "systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs" features=item.system.features label=(localize "DAGGERHEART.GENERAL.features")}} + {{> "systems/daggerheart/templates/ui/tooltip/parts/beastformData.hbs" }}

{{localize "DAGGERHEART.UI.Tooltip.middleClick"}} diff --git a/templates/ui/tooltip/effect-display.hbs b/templates/ui/tooltip/effect-display.hbs index ebb21be4..f1db03c5 100644 --- a/templates/ui/tooltip/effect-display.hbs +++ b/templates/ui/tooltip/effect-display.hbs @@ -41,26 +41,32 @@ {{/if}} - {{#unless effect.isLockedCondition}} +

- {{#if effect.system.stacking}} + {{#if (eq effect.type 'beastform')}}

- {{localize "DAGGERHEART.UI.EffectsDisplay.increaseStacks"}} + {{localize "DAGGERHEART.UI.Tooltip.middleClick"}}

- {{#if (gt effect.system.stacking.value 1)}} + {{/if}} + {{#unless effect.isLockedCondition}} + {{#if effect.system.stacking}}

- {{localize "DAGGERHEART.UI.EffectsDisplay.decreaseStacks"}} + {{localize "DAGGERHEART.UI.EffectsDisplay.increaseStacks"}}

+ {{#if (gt effect.system.stacking.value 1)}} +

+ {{localize "DAGGERHEART.UI.EffectsDisplay.decreaseStacks"}} +

+ {{else}} +

+ {{localize "DAGGERHEART.UI.EffectsDisplay.removeThing" thing=(localize "DAGGERHEART.GENERAL.Effect.single")}} +

+ {{/if}} {{else}}

{{localize "DAGGERHEART.UI.EffectsDisplay.removeThing" thing=(localize "DAGGERHEART.GENERAL.Effect.single")}}

{{/if}} - {{else}} -

- {{localize "DAGGERHEART.UI.EffectsDisplay.removeThing" thing=(localize "DAGGERHEART.GENERAL.Effect.single")}} -

- {{/if}} + {{/unless}}
- {{/unless}} \ No newline at end of file diff --git a/templates/ui/tooltip/parts/beastformData.hbs b/templates/ui/tooltip/parts/beastformData.hbs new file mode 100644 index 00000000..0473df40 --- /dev/null +++ b/templates/ui/tooltip/parts/beastformData.hbs @@ -0,0 +1,31 @@ +{{#if description}} +
{{{description}}}
+{{/if}} + +
+ {{#with item.system.beastformAttackData}} +
+ {{localize "DAGGERHEART.ITEMS.Beastform.mainTrait"}} {{this.trait}} +
+
+ {{localize "DAGGERHEART.ITEMS.Beastform.traitBonus"}} {{this.traitBonus}} +
+
+ {{localize "DAGGERHEART.GENERAL.evasion"}} {{this.evasionBonus}} +
+
+ {{localize "DAGGERHEART.GENERAL.damage"}} {{concat this.damageDice ' ' this.damageBonus}} +
+ {{/with}} +
+ +

{{localize "DAGGERHEART.ITEMS.Beastform.FIELDS.advantageOn.label"}}

+
+ {{#each item.system.advantageOn as | chip |}} +
+ {{ifThen chip.value chip.value chip}} +
+ {{/each}} +
+ +{{> "systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs" features=item.system.features label=(localize "DAGGERHEART.GENERAL.features")}} \ No newline at end of file diff --git a/templates/ui/tooltip/parts/tooltipTags.hbs b/templates/ui/tooltip/parts/tooltipTags.hbs index 6a6d126d..da32723f 100644 --- a/templates/ui/tooltip/parts/tooltipTags.hbs +++ b/templates/ui/tooltip/parts/tooltipTags.hbs @@ -1,4 +1,5 @@ {{#if (gt features.length 0)}}

{{label}}

{{/if}} +
{{#each features as | feature |}} {{#with (ifThen ../isAction feature (ifThen feature.item feature.item feature))}} From 2a294684d4d70a3d1984facac75a67c7f89acc46 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sat, 28 Mar 2026 00:38:50 +0100 Subject: [PATCH 11/19] Remade branch (#1754) --- lang/en.json | 11 +- .../applications/dialogs/beastformDialog.mjs | 38 +++ .../sheets-configs/action-base-config.mjs | 19 +- module/data/fields/action/beastformField.mjs | 23 +- .../feature_Evolution_6rlxhrRwFaVgq9fe.json | 270 +----------------- styles/less/dialog/beastform/sheet.less | 38 +++ styles/less/sheets/actions/actions.less | 14 + templates/actionTypes/beastform.hbs | 16 +- templates/dialogs/beastform/modifications.hbs | 28 ++ 9 files changed, 182 insertions(+), 275 deletions(-) create mode 100644 templates/dialogs/beastform/modifications.hbs diff --git a/lang/en.json b/lang/en.json index 5dc09a1e..d341b4bf 100755 --- a/lang/en.json +++ b/lang/en.json @@ -89,9 +89,14 @@ }, "Config": { "beastform": { - "exact": "Beastform Max Tier", - "exactHint": "The Character's Tier is used if empty", - "label": "Beastform" + "exact": { "label": "Beastform Max Tier", "hint": "The Character's Tier is used if empty" }, + "modifications": { + "traitBonuses": { + "label": { "single": "Trait Bonus", "plural": "Trait Bonuses" }, + "hint": "Pick bonuses you apply to freely chosen traits at the time of transforming", + "bonus": "Bonus Amount" + } + } }, "countdown": { "defaultOwnership": "Default Ownership", diff --git a/module/applications/dialogs/beastformDialog.mjs b/module/applications/dialogs/beastformDialog.mjs index 09a9222b..8ae6d5fe 100644 --- a/module/applications/dialogs/beastformDialog.mjs +++ b/module/applications/dialogs/beastformDialog.mjs @@ -10,6 +10,12 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat this.selected = null; this.evolved = { form: null }; this.hybrid = { forms: {}, advantages: {}, features: {} }; + this.modifications = { + traitBonuses: configData.modifications.traitBonuses.map(x => ({ + trait: null, + bonus: x.bonus + })) + }; this._dragDrop = this._createDragDropHandlers(); } @@ -28,6 +34,7 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat selectBeastform: this.selectBeastform, toggleHybridFeature: this.toggleHybridFeature, toggleHybridAdvantage: this.toggleHybridAdvantage, + toggleTraitBonus: this.toggleTraitBonus, submitBeastform: this.submitBeastform }, form: { @@ -48,6 +55,7 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat tabs: { template: 'systems/daggerheart/templates/dialogs/beastform/tabs.hbs' }, beastformTier: { template: 'systems/daggerheart/templates/dialogs/beastform/beastformTier.hbs' }, advanced: { template: 'systems/daggerheart/templates/dialogs/beastform/advanced.hbs' }, + modifications: { template: 'systems/daggerheart/templates/dialogs/beastform/modifications.hbs' }, footer: { template: 'systems/daggerheart/templates/dialogs/beastform/footer.hbs' } }; @@ -146,6 +154,9 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat {} ); + context.modifications = this.modifications; + context.traits = CONFIG.DH.ACTOR.abilities; + context.tier = beastformTiers[this.tabGroups.primary]; context.tierKey = this.tabGroups.primary; @@ -155,6 +166,9 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat } canSubmit() { + const modificationsFinished = this.modifications.traitBonuses.every(x => x.trait); + if (!modificationsFinished) return false; + if (this.selected) { switch (this.selected.system.beastformType) { case 'normal': @@ -261,6 +275,13 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat this.render(); } + static toggleTraitBonus(_, button) { + const { index, trait } = button.dataset; + this.modifications.traitBonuses[index].trait = + this.modifications.traitBonuses[index].trait === trait ? null : trait; + this.render(); + } + static async submitBeastform() { await this.close({ submitted: true }); } @@ -292,6 +313,23 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat } } + const beastformEffect = selected.effects.find(x => x.type === 'beastform'); + for (const traitBonus of app.modifications.traitBonuses) { + const existingChange = beastformEffect.changes.find( + x => x.key === `system.traits.${traitBonus.trait}.value` + ); + if (existingChange) { + existingChange.value = Number.parseInt(existingChange.value) + traitBonus.bonus; + } else { + beastformEffect.changes.push({ + key: `system.traits.${traitBonus.trait}.value`, + mode: 2, + priority: null, + value: traitBonus.bonus + }); + } + } + resolve({ selected: selected, evolved: { ...app.evolved, form: evolved }, diff --git a/module/applications/sheets-configs/action-base-config.mjs b/module/applications/sheets-configs/action-base-config.mjs index 05a3177d..0ae39477 100644 --- a/module/applications/sheets-configs/action-base-config.mjs +++ b/module/applications/sheets-configs/action-base-config.mjs @@ -36,7 +36,9 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) editDoc: this.editDoc, addTrigger: this.addTrigger, removeTrigger: this.removeTrigger, - expandTrigger: this.expandTrigger + expandTrigger: this.expandTrigger, + addBeastformTraitBonus: this.addBeastformTraitBonus, + removeBeastformTraitBonus: this.removeBeastformTraitBonus }, form: { handler: this.updateForm, @@ -412,6 +414,21 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) } } + static async addBeastformTraitBonus() { + const data = this.action.toObject(); + data.beastform.modifications.traitBonuses = [ + ...data.beastform.modifications.traitBonuses, + this.action.schema.fields.beastform.fields.modifications.fields.traitBonuses.element.getInitialValue() + ]; + this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); + } + + static async removeBeastformTraitBonus(_event, button) { + const data = this.action.toObject(); + data.beastform.modifications.traitBonuses.splice(button.dataset.index, 1); + this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); + } + updateSummonCount(event) { event.stopPropagation(); const wrapper = event.target.closest('.summon-count-wrapper'); diff --git a/module/data/fields/action/beastformField.mjs b/module/data/fields/action/beastformField.mjs index e19807c7..e3be9937 100644 --- a/module/data/fields/action/beastformField.mjs +++ b/module/data/fields/action/beastformField.mjs @@ -28,8 +28,21 @@ export default class BeastformField extends fields.SchemaField { { 1: game.i18n.localize('DAGGERHEART.GENERAL.Tiers.1') } ); }, - hint: 'DAGGERHEART.ACTIONS.Config.beastform.exactHint' + label: 'DAGGERHEART.ACTIONS.Config.beastform.exact.label', + hint: 'DAGGERHEART.ACTIONS.Config.beastform.exact.hint' }) + }), + modifications: new fields.SchemaField({ + traitBonuses: new fields.ArrayField( + new fields.SchemaField({ + bonus: new fields.NumberField({ + integer: true, + initial: 1, + min: 1, + label: 'DAGGERHEART.ACTIONS.Config.beastform.modifications.traitBonuses.bonus' + }) + }) + ) }) }; super(beastformFields, options, context); @@ -66,15 +79,9 @@ export default class BeastformField extends fields.SchemaField { ) ?? 1; config.tierLimit = this.beastform.tierAccess.exact ?? actorTier; + config.modifications = this.beastform.modifications; } - /** - * TODO by Harry - * @param {*} selectedForm - * @param {*} evolvedData - * @param {*} hybridData - * @returns - */ static async transform(selectedForm, evolvedData, hybridData) { const formData = evolvedData?.form ?? selectedForm; const beastformEffect = formData.effects.find(x => x.type === 'beastform'); diff --git a/src/packs/classes/feature_Evolution_6rlxhrRwFaVgq9fe.json b/src/packs/classes/feature_Evolution_6rlxhrRwFaVgq9fe.json index 46380fe8..421063a4 100644 --- a/src/packs/classes/feature_Evolution_6rlxhrRwFaVgq9fe.json +++ b/src/packs/classes/feature_Evolution_6rlxhrRwFaVgq9fe.json @@ -5,7 +5,7 @@ "_id": "6rlxhrRwFaVgq9fe", "img": "icons/magic/nature/wolf-paw-glow-large-orange.webp", "system": { - "description": "

Spend 3 Hope to transform into a Beastform without marking a Stress. When you do, choose one trait to raise by +1 until you drop out of that Beastform.

Note: Toggle one of the Evolution Traits in the effects tab to raise a trait by 1, e.g. Evolution: Agility

", + "description": "

Spend 3 Hope to transform into a Beastform without marking a Stress. When you do, choose one trait to raise by +1 until you drop out of that Beastform.

", "resource": null, "actions": { "bj4m9E8ObFT0xDQ4": { @@ -31,6 +31,13 @@ "beastform": { "tierAccess": { "exact": null + }, + "modifications": { + "traitBonuses": [ + { + "bonus": 1 + } + ] } }, "name": "Beastform", @@ -46,266 +53,7 @@ "artist": "" } }, - "effects": [ - { - "name": "Evolution: Agility", - "type": "base", - "system": { - "rangeDependence": { - "enabled": false, - "type": "withinRange", - "target": "hostile", - "range": "melee" - } - }, - "_id": "vQOqLZAxOltAzsVv", - "img": "icons/magic/nature/wolf-paw-glow-large-orange.webp", - "changes": [ - { - "key": "system.traits.agility.value", - "mode": 2, - "value": "1", - "priority": null - } - ], - "disabled": true, - "duration": { - "startTime": null, - "combat": null, - "seconds": null, - "rounds": null, - "turns": null, - "startRound": null, - "startTurn": null - }, - "description": "

Toggle this for +1 to Agility when using Evolution. Turn it off when you leave Beastform.

", - "origin": null, - "tint": "#ffffff", - "transfer": true, - "statuses": [], - "sort": 0, - "flags": {}, - "_stats": { - "compendiumSource": null - }, - "_key": "!items.effects!6rlxhrRwFaVgq9fe.vQOqLZAxOltAzsVv" - }, - { - "name": "Evolution: Strength", - "type": "base", - "system": { - "rangeDependence": { - "enabled": false, - "type": "withinRange", - "target": "hostile", - "range": "melee" - } - }, - "_id": "cwEsO1NZpkQHuoTT", - "img": "icons/magic/nature/wolf-paw-glow-large-orange.webp", - "changes": [ - { - "key": "system.traits.strength.value", - "mode": 2, - "value": "1", - "priority": null - } - ], - "disabled": true, - "duration": { - "startTime": null, - "combat": null, - "seconds": null, - "rounds": null, - "turns": null, - "startRound": null, - "startTurn": null - }, - "description": "

Toggle this for +1 to Strength when using Evolution. Turn it off when you leave Beastform.

", - "origin": null, - "tint": "#ffffff", - "transfer": true, - "statuses": [], - "sort": 0, - "flags": {}, - "_stats": { - "compendiumSource": null - }, - "_key": "!items.effects!6rlxhrRwFaVgq9fe.cwEsO1NZpkQHuoTT" - }, - { - "name": "Evolution: Finesse", - "type": "base", - "system": { - "rangeDependence": { - "enabled": false, - "type": "withinRange", - "target": "hostile", - "range": "melee" - } - }, - "_id": "8P0nwRHNsVnHVPjq", - "img": "icons/magic/nature/wolf-paw-glow-large-orange.webp", - "changes": [ - { - "key": "system.traits.finesse.value", - "mode": 2, - "value": "1", - "priority": null - } - ], - "disabled": true, - "duration": { - "startTime": null, - "combat": null, - "seconds": null, - "rounds": null, - "turns": null, - "startRound": null, - "startTurn": null - }, - "description": "

Toggle this for +1 to Finesse when using Evolution. Turn it off when you leave Beastform.

", - "origin": null, - "tint": "#ffffff", - "transfer": true, - "statuses": [], - "sort": 0, - "flags": {}, - "_stats": { - "compendiumSource": null - }, - "_key": "!items.effects!6rlxhrRwFaVgq9fe.8P0nwRHNsVnHVPjq" - }, - { - "name": "Evolution: Instinct", - "type": "base", - "system": { - "rangeDependence": { - "enabled": false, - "type": "withinRange", - "target": "hostile", - "range": "melee" - } - }, - "_id": "i2GhNGo5TnGtLuA0", - "img": "icons/magic/nature/wolf-paw-glow-large-orange.webp", - "changes": [ - { - "key": "system.traits.instinct.value", - "mode": 2, - "value": "1", - "priority": null - } - ], - "disabled": true, - "duration": { - "startTime": null, - "combat": null, - "seconds": null, - "rounds": null, - "turns": null, - "startRound": null, - "startTurn": null - }, - "description": "

Toggle this for +1 to Instinct when using Evolution. Turn it off when you leave Beastform.

", - "origin": null, - "tint": "#ffffff", - "transfer": true, - "statuses": [], - "sort": 0, - "flags": {}, - "_stats": { - "compendiumSource": null - }, - "_key": "!items.effects!6rlxhrRwFaVgq9fe.i2GhNGo5TnGtLuA0" - }, - { - "name": "Evolution: Presence", - "type": "base", - "system": { - "rangeDependence": { - "enabled": false, - "type": "withinRange", - "target": "hostile", - "range": "melee" - } - }, - "_id": "APQF1in1LXjBZh9n", - "img": "icons/magic/nature/wolf-paw-glow-large-orange.webp", - "changes": [ - { - "key": "system.traits.presence.value", - "mode": 2, - "value": "1", - "priority": null - } - ], - "disabled": true, - "duration": { - "startTime": null, - "combat": null, - "seconds": null, - "rounds": null, - "turns": null, - "startRound": null, - "startTurn": null - }, - "description": "

Toggle this for +1 to Presence when using Evolution. Turn it off when you leave Beastform.

", - "origin": null, - "tint": "#ffffff", - "transfer": true, - "statuses": [], - "sort": 0, - "flags": {}, - "_stats": { - "compendiumSource": null - }, - "_key": "!items.effects!6rlxhrRwFaVgq9fe.APQF1in1LXjBZh9n" - }, - { - "name": "Evolution: Knowledge", - "type": "base", - "system": { - "rangeDependence": { - "enabled": false, - "type": "withinRange", - "target": "hostile", - "range": "melee" - } - }, - "_id": "WwOvGJYJb4d37cOy", - "img": "icons/magic/nature/wolf-paw-glow-large-orange.webp", - "changes": [ - { - "key": "system.traits.knowledge.value", - "mode": 2, - "value": "1", - "priority": null - } - ], - "disabled": true, - "duration": { - "startTime": null, - "combat": null, - "seconds": null, - "rounds": null, - "turns": null, - "startRound": null, - "startTurn": null - }, - "description": "

Toggle this for +1 to Knowledge when using Evolution. Turn it off when you leave Beastform.

", - "origin": null, - "tint": "#ffffff", - "transfer": true, - "statuses": [], - "sort": 0, - "flags": {}, - "_stats": { - "compendiumSource": null - }, - "_key": "!items.effects!6rlxhrRwFaVgq9fe.WwOvGJYJb4d37cOy" - } - ], + "effects": [], "sort": 100000, "ownership": { "default": 0, diff --git a/styles/less/dialog/beastform/sheet.less b/styles/less/dialog/beastform/sheet.less index 9e87f53b..0e1fe746 100644 --- a/styles/less/dialog/beastform/sheet.less +++ b/styles/less/dialog/beastform/sheet.less @@ -204,6 +204,44 @@ } } + .modifications-container { + display: flex; + flex-direction: column; + gap: 16px; + + .trait-bonuses-container { + display: flex; + flex-direction: column; + gap: 8px; + + .bonus-separator { + background: light-dark(@dark-blue, @golden); + mask-image: linear-gradient(270deg, transparent 0%, black 50%, transparent 100%); + height: 2px; + width: calc(100% - 10px); + } + + .trait-bonus-container { + display: flex; + gap: 4px; + + .trait-card { + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + padding: 2px; + opacity: 0.4; + flex: 1; + white-space: nowrap; + text-align: center; + + &.selected { + opacity: 1; + } + } + } + } + } + footer { margin-top: 8px; display: flex; diff --git a/styles/less/sheets/actions/actions.less b/styles/less/sheets/actions/actions.less index 5c21dc60..5a18d622 100644 --- a/styles/less/sheets/actions/actions.less +++ b/styles/less/sheets/actions/actions.less @@ -133,4 +133,18 @@ height: 300px; } } + + .deletable-row { + display: flex; + align-items: end; + gap: 8px; + + input { + flex: 1; + } + + a { + padding-bottom: 7px; + } + } } diff --git a/templates/actionTypes/beastform.hbs b/templates/actionTypes/beastform.hbs index b9bea445..885038a1 100644 --- a/templates/actionTypes/beastform.hbs +++ b/templates/actionTypes/beastform.hbs @@ -1,4 +1,16 @@ +{{formGroup fields.tierAccess.fields.exact value=source.tierAccess.exact name="beastform.tierAccess.exact" labelAttr="label" valueAttr="key" localize=true blank=""}} +
- {{localize "DAGGERHEART.ACTIONS.Config.beastform.label"}} - {{formGroup fields.tierAccess.fields.exact value=source.tierAccess.exact name="beastform.tierAccess.exact" labelAttr="label" valueAttr="key" localize=true blank=""}} + {{localize "DAGGERHEART.ACTIONS.Config.beastform.modifications.traitBonuses.label.plural"}} + + {{#if source.modifications.traitBonuses.length}} + {{#each source.modifications.traitBonuses as |traitBonus index|}} +
+ {{formGroup ../fields.modifications.fields.traitBonuses.element.fields.bonus value=traitBonus.bonus name=(concat "beastform.modifications.traitBonuses." index ".bonus") localize=true}} + +
+ {{/each}} + {{else}} + {{localize "DAGGERHEART.ACTIONS.Config.beastform.modifications.traitBonuses.hint"}} + {{/if}}
\ No newline at end of file diff --git a/templates/dialogs/beastform/modifications.hbs b/templates/dialogs/beastform/modifications.hbs new file mode 100644 index 00000000..2563ea13 --- /dev/null +++ b/templates/dialogs/beastform/modifications.hbs @@ -0,0 +1,28 @@ +
+ {{#if modifications.traitBonuses.length}} +
+ + {{#if (gt modifications.traitBonuses.length 1)}} + {{localize "DAGGERHEART.ACTIONS.Config.beastform.modifications.traitBonuses.label.plural"}} + {{else}} + {{localize "DAGGERHEART.ACTIONS.Config.beastform.modifications.traitBonuses.label.single"}} + {{/if}} + + +
+ {{#each modifications.traitBonuses as |traitBonus index|}} +
+ {{#each @root.traits as |trait|}} + + {{localize trait.label}} +{{traitBonus.bonus}} + + {{/each}} +
+ {{#unless @last}}
{{/unless}} + {{/each}} +
+
+ {{/if}} +
\ No newline at end of file From 24d22dde59728d3a8785c1ef5a912351fdb3bfc6 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sat, 28 Mar 2026 03:01:50 +0100 Subject: [PATCH 12/19] [V14] [Feature] Spotlight Without Combat (#1755) --- lang/en.json | 3 +- module/applications/ui/combatTracker.mjs | 11 ++++- module/canvas/placeables/token.mjs | 30 +++++++++++++ module/config/settingsConfig.mjs | 3 +- module/data/_module.mjs | 1 + module/data/spotlightTracker.mjs | 9 ++++ module/macros/spotlightCombatant.mjs | 57 ++++++++++++++++++------ module/systemRegistration/settings.mjs | 14 +++++- 8 files changed, 109 insertions(+), 19 deletions(-) create mode 100644 module/data/spotlightTracker.mjs diff --git a/lang/en.json b/lang/en.json index d341b4bf..cb020a93 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2555,8 +2555,7 @@ "MACROS": { "Spotlight": { "errors": { - "noActiveCombat": "There is no active encounter", - "noCombatantSelected": "A combatant token must be either selected or hovered to spotlight it" + "noTokenSelected": "A token on the canvas must either be selected or hovered to spotlight it" } } }, diff --git a/module/applications/ui/combatTracker.mjs b/module/applications/ui/combatTracker.mjs index 345c6fcd..1043e128 100644 --- a/module/applications/ui/combatTracker.mjs +++ b/module/applications/ui/combatTracker.mjs @@ -1,5 +1,6 @@ import { AdversaryBPPerEncounter } from '../../config/encounterConfig.mjs'; import { expireActiveEffects } from '../../helpers/utils.mjs'; +import { clearPreviousSpotlight } from '../../macros/spotlightCombatant.mjs'; export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker { static DEFAULT_OPTIONS = { @@ -150,13 +151,13 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C } async setCombatantSpotlight(combatantId) { + const combatant = this.viewed.combatants.get(combatantId); const update = { system: { 'spotlight.requesting': false, 'spotlight.requestOrderIndex': 0 } }; - const combatant = this.viewed.combatants.get(combatantId); const toggleTurn = this.viewed.combatants.contents .sort(this.viewed._sortCombatants) @@ -187,6 +188,14 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C round: this.viewed.round + 1 }); await combatant.update(update); + if (combatant.token) clearPreviousSpotlight(); + } + + async clearTurn() { + await this.viewed.update({ + turn: null, + round: this.viewed.round + 1 + }); } static async requestSpotlight(_, target) { diff --git a/module/canvas/placeables/token.mjs b/module/canvas/placeables/token.mjs index 35d34f83..77d8c3f4 100644 --- a/module/canvas/placeables/token.mjs +++ b/module/canvas/placeables/token.mjs @@ -10,6 +10,36 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { this.previewHelp ||= this.addChild(this.#drawPreviewHelp()); } + /**@inheritdoc */ + _refreshTurnMarker() { + // Should a Turn Marker be active? + const { turnMarker } = this.document; + const markersEnabled = + CONFIG.Combat.settings.turnMarker.enabled && turnMarker.mode !== CONST.TOKEN_TURN_MARKER_MODES.DISABLED; + const spotlighted = game.settings + .get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.SpotlightTracker) + .spotlightedTokens.has(this.document.uuid); + + const turnIsSet = game.combat?.turn !== null; + const isTurn = game.combat?.combatant?.tokenId === this.id; + const markerActive = markersEnabled && turnIsSet ? isTurn : spotlighted; + + // Activate a Turn Marker + if (markerActive) { + if (!this.turnMarker) + this.turnMarker = this.addChildAt(new foundry.canvas.placeables.tokens.TokenTurnMarker(this), 0); + canvas.tokens.turnMarkers.add(this); + this.turnMarker.draw(); + } + + // Remove a Turn Marker + else if (this.turnMarker) { + canvas.tokens.turnMarkers.delete(this); + this.turnMarker.destroy(); + this.turnMarker = null; + } + } + /** @inheritDoc */ async _drawEffects() { this.effects.renderable = false; diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index 36892731..de4d96be 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -39,7 +39,8 @@ export const gameSettings = { Countdowns: 'Countdowns', LastMigrationVersion: 'LastMigrationVersion', SpotlightRequestQueue: 'SpotlightRequestQueue', - CompendiumBrowserSettings: 'CompendiumBrowserSettings' + CompendiumBrowserSettings: 'CompendiumBrowserSettings', + SpotlightTracker: 'SpotlightTracker' }; export const actionAutomationChoices = { diff --git a/module/data/_module.mjs b/module/data/_module.mjs index 43ff7807..0e7e295e 100644 --- a/module/data/_module.mjs +++ b/module/data/_module.mjs @@ -4,6 +4,7 @@ export { default as DhRollTable } from './rollTable.mjs'; export { default as RegisteredTriggers } from './registeredTriggers.mjs'; export { default as CompendiumBrowserSettings } from './compendiumBrowserSettings.mjs'; export { default as TagTeamData } from './tagTeamData.mjs'; +export { default as SpotlightTracker } from './spotlightTracker.mjs'; export * as countdowns from './countdowns.mjs'; export * as actions from './action/_module.mjs'; diff --git a/module/data/spotlightTracker.mjs b/module/data/spotlightTracker.mjs new file mode 100644 index 00000000..57f54e16 --- /dev/null +++ b/module/data/spotlightTracker.mjs @@ -0,0 +1,9 @@ +export default class SpotlightTracker extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + spotlightedTokens: new fields.SetField(new fields.DocumentUUIDField()) + }; + } +} diff --git a/module/macros/spotlightCombatant.mjs b/module/macros/spotlightCombatant.mjs index 68a26ff9..dc8339ac 100644 --- a/module/macros/spotlightCombatant.mjs +++ b/module/macros/spotlightCombatant.mjs @@ -1,21 +1,50 @@ /** - * Spotlights a combatant. - * The combatant can be selected in a number of ways. If many are applied at the same time, the following order is used: - * 1) SelectedCombatant - * 2) HoveredCombatant + * Spotlight a token on the canvas. If it is a combatant, run it through combatTracker's spotlight logic. + * @param {TokenDocument} token - The token to spotlight + * @returns {void} */ -const spotlightCombatant = () => { - if (!game.combat) - return ui.notifications.error(game.i18n.localize('DAGGERHEART.MACROS.Spotlight.errors.noActiveCombat')); +const spotlightCombatantMacro = async token => { + if (!token) + return ui.notifications.error(game.i18n.localize('DAGGERHEART.MACROS.Spotlight.errors.noTokenSelected')); - const selectedCombatant = canvas.tokens.controlled.length > 0 ? canvas.tokens.controlled[0].combatant : null; - const hoveredCombatant = game.canvas.tokens.hover?.combatant; + const combatantCombat = token.combatant + ? game.combat + : game.combats.find(combat => combat.combatants.some(x => x.token && x.token.id === token.document.id)); + if (combatantCombat) { + const combatant = combatantCombat.combatants.find(x => x.token.id === token.document.id); + if (!combatantCombat.active) { + await combatantCombat.activate(); + if (combatantCombat.combatant?.id !== combatant.id) ui.combat.setCombatantSpotlight(combatant.id); + } else { + ui.combat.setCombatantSpotlight(combatant.id); + } + } else { + if (game.combat) await ui.combat.clearTurn(); - const combatant = selectedCombatant ?? hoveredCombatant; - if (!combatant) - return ui.notifications.error(game.i18n.localize('DAGGERHEART.MACROS.Spotlight.errors.noCombatantSelected')); + const spotlightTracker = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.SpotlightTracker); + const isSpotlighted = spotlightTracker.spotlightedTokens.has(token.document.uuid); + if (!isSpotlighted) await clearPreviousSpotlight(); - ui.combat.setCombatantSpotlight(combatant.id); + spotlightTracker.updateSource({ + spotlightedTokens: isSpotlighted ? [] : [token.document.uuid] + }); + + await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.SpotlightTracker, spotlightTracker); + token.renderFlags.set({ refreshTurnMarker: true }); + } }; -export default spotlightCombatant; +export const clearPreviousSpotlight = async () => { + const spotlightTracker = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.SpotlightTracker); + const previouslySpotlightedUuid = + spotlightTracker.spotlightedTokens.size > 0 ? spotlightTracker.spotlightedTokens.first() : null; + if (!previouslySpotlightedUuid) return; + + spotlightTracker.updateSource({ spotlightedTokens: [] }); + await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.SpotlightTracker, spotlightTracker); + + const previousToken = await foundry.utils.fromUuid(previouslySpotlightedUuid); + previousToken.object.renderFlags.set({ refreshTurnMarker: true }); +}; + +export default spotlightCombatantMacro; diff --git a/module/systemRegistration/settings.mjs b/module/systemRegistration/settings.mjs index 5eb9dad1..63611cda 100644 --- a/module/systemRegistration/settings.mjs +++ b/module/systemRegistration/settings.mjs @@ -16,6 +16,7 @@ import { DhVariantRuleSettings } from '../applications/settings/_module.mjs'; import { CompendiumBrowserSettings } from '../data/_module.mjs'; +import SpotlightTracker from '../data/spotlightTracker.mjs'; export const registerDHSettings = () => { registerKeyBindings(); @@ -40,7 +41,12 @@ export const registerKeyBindings = () => { hint: game.i18n.localize('DAGGERHEART.SETTINGS.Keybindings.spotlight.hint'), uneditable: [], editable: [], - onDown: game.system.api.macros.spotlightCombatant, + onDown: () => { + const selectedTokens = canvas.tokens.controlled.length > 0 ? canvas.tokens.controlled[0] : null; + const hoveredTokens = game.canvas.tokens.hover ? game.canvas.tokens.hover : null; + const tokens = selectedTokens ?? hoveredTokens; + game.system.api.macros.spotlightCombatant(tokens); + }, onUp: () => {}, restricted: true, reservedModifiers: [], @@ -177,4 +183,10 @@ const registerNonConfigSettings = () => { config: false, type: CompendiumBrowserSettings }); + + game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.SpotlightTracker, { + scope: 'world', + config: false, + type: SpotlightTracker + }); }; From 740216ada2bc86d2a5b5aa517101649bd6006352 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Sat, 28 Mar 2026 03:09:41 +0100 Subject: [PATCH 13/19] Fixed spotlight case outside of combat --- module/canvas/placeables/token.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/canvas/placeables/token.mjs b/module/canvas/placeables/token.mjs index 77d8c3f4..77c178d6 100644 --- a/module/canvas/placeables/token.mjs +++ b/module/canvas/placeables/token.mjs @@ -20,7 +20,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { .get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.SpotlightTracker) .spotlightedTokens.has(this.document.uuid); - const turnIsSet = game.combat?.turn !== null; + const turnIsSet = typeof game.combat?.turn === 'number'; const isTurn = game.combat?.combatant?.tokenId === this.id; const markerActive = markersEnabled && turnIsSet ? isTurn : spotlighted; From e8f052faf3b5f671c1490600942cfc2c17bf8356 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Sun, 29 Mar 2026 11:07:16 +0200 Subject: [PATCH 14/19] Fixed drag/drop on application-sheet --- .../sheets/api/application-mixin.mjs | 35 ++++++------------- .../global/partials/inventory-item-V2.hbs | 2 +- 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 83313454..64f62405 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -79,8 +79,6 @@ export default function DHApplicationMixin(Base) { */ constructor(options = {}) { super(options); - - this._setupDragDrop(); } /** @@ -175,9 +173,6 @@ export default function DHApplicationMixin(Base) { _attachPartListeners(partId, htmlElement, options) { super._attachPartListeners(partId, htmlElement, options); - /* Core dragDrop from ActorDocument is always only 1. Possible we could refactor our own */ - if (Array.isArray(this._dragDrop)) this._dragDrop.forEach(d => d.bind(htmlElement)); - // Handle delta inputs for (const deltaInput of htmlElement.querySelectorAll('input[data-allow-delta]')) { deltaInput.dataset.numValue = deltaInput.value; @@ -289,6 +284,16 @@ export default function DHApplicationMixin(Base) { async _onRender(context, options) { await super._onRender(context, options); this._createTagifyElements(this.options.tagifyConfigs); + + for (const d of this.options.dragDrop) { + new foundry.applications.ux.DragDrop.implementation({ + ...d, + callbacks: { + dragstart: this._onDragStart.bind(this), + drop: this._onDrop.bind(this) + } + }).bind(this.element); + } } /* -------------------------------------------- */ @@ -349,26 +354,6 @@ export default function DHApplicationMixin(Base) { /* Drag and Drop */ /* -------------------------------------------- */ - /** - * Creates drag-drop handlers from the configured options. - * @returns {foundry.applications.ux.DragDrop[]} - * @private - */ - _setupDragDrop() { - if (this._dragDrop) { - this._dragDrop.callbacks.dragStart = this._onDragStart; - this._dragDrop.callback.drop = this._onDrop; - } else { - this._dragDrop = this.options.dragDrop.map(d => { - d.callbacks = { - dragstart: this._onDragStart.bind(this), - drop: this._onDrop.bind(this) - }; - return new foundry.applications.ux.DragDrop.implementation(d); - }); - } - } - /** * Handle dragStart event. * @param {DragEvent} event diff --git a/templates/sheets/global/partials/inventory-item-V2.hbs b/templates/sheets/global/partials/inventory-item-V2.hbs index a758a28f..2129b969 100644 --- a/templates/sheets/global/partials/inventory-item-V2.hbs +++ b/templates/sheets/global/partials/inventory-item-V2.hbs @@ -17,7 +17,7 @@ Parameters: - showActions {boolean} : If true show feature's actions. --}} -
  • Date: Sun, 29 Mar 2026 12:50:34 +0200 Subject: [PATCH 15/19] Removed a temporary override function in document/token. Doesn't seem needed anymore, and it was outdated, making bar2 on tokens never visualy update --- module/documents/token.mjs | 58 -------------------------------------- 1 file changed, 58 deletions(-) diff --git a/module/documents/token.mjs b/module/documents/token.mjs index 8e810689..4ee7ce05 100644 --- a/module/documents/token.mjs +++ b/module/documents/token.mjs @@ -494,62 +494,4 @@ export default class DHToken extends CONFIG.Token.documentClass { game.system.registeredTriggers.unregisterItemTriggers(this.actor.items); } } - - /* V14 TEMP until foundry fixes: https://discord.com/channels/170995199584108546/1421197211194228907/1467296028700049566 */ - _onRelatedUpdate(update = {}, operation = {}) { - this.#refreshOverrides(operation); - this._prepareBars(); - - // Update tracked Combat resource - const combatant = this.combatant; - if (combatant) { - const isActorUpdate = [this, null, undefined].includes(operation.parent); - const resource = game.combat.settings.resource; - const updates = Array.isArray(update) ? update : [update]; - if (isActorUpdate && resource && updates.some(u => foundry.utils.hasProperty(u.system ?? {}, resource))) { - combatant.updateResource(); - } - ui.combat.render(); - } - - // Trigger redraws on the token - if (this.parent.isView) { - if (this.object?.hasActiveHUD) canvas.tokens.hud.render(); - this.object?.renderFlags.set({ redrawEffects: true }); - for (const key of ['bar1', 'bar2']) { - const name = `${this.object?.objectId}.animate${key.capitalize()}`; - const easing = foundry.canvas.animation.CanvasAnimation.easeInOutCosine; - this.object?.animate({ [key]: this[key] }, { name, easing }); - } - for (const app of foundry.applications.sheets.TokenConfig.instances()) { - app._preview?.updateSource({ delta: this.toObject().delta }, { diff: false, recursive: false }); - app._preview?.object?.renderFlags.set({ refreshBars: true, redrawEffects: true }); - } - } - } - - /* V14 TEMP until foundry fixes: https://discord.com/channels/170995199584108546/1421197211194228907/1467296028700049566 */ - #refreshOverrides(operation) { - if (!this.actor) return; - - const { deepClone, mergeObject, equals, isEmpty } = foundry.utils; - const oldOverrides = deepClone(this._overrides) ?? {}; - const newOverrides = deepClone(this.actor?.tokenOverrides ?? {}, { prune: true }); - if (!equals(oldOverrides, newOverrides)) { - this._overrides = newOverrides; - this.reset(); - - // Send emulated update data to the PlaceableObject - if (!canvas.ready || canvas.scene !== this.scene) return; - const { width, height, depth, ...changes } = mergeObject( - mergeObject(oldOverrides, this, { insertKeys: false, insertValues: false }), - this._overrides - ); - this.object?._onUpdate(changes, {}, game.user.id); - - // Hand off size changes to a secondary handler requiring downstream implementation. - const sizeChanges = deepClone({ width, height, depth }, { prune: true }); - if (!isEmpty(sizeChanges)) this._onOverrideSize(sizeChanges, operation); - } - } } From dbd5ef8bb0b689510589b5de6348f5a981210bed Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sun, 29 Mar 2026 23:54:45 +0200 Subject: [PATCH 16/19] Added fall and collision damage buttons in the GM Menu (#1756) --- lang/en.json | 9 ++++++- module/applications/sheets/api/base-actor.mjs | 1 - .../sidebar/tabs/daggerheartMenu.mjs | 22 ++++++++++++++- module/config/generalConfig.mjs | 27 +++++++++++++++++++ module/documents/item.mjs | 1 - styles/less/ui/sidebar/daggerheartMenu.less | 6 ++--- templates/sidebar/daggerheart-menu/main.hbs | 20 +++++++++++--- templates/ui/chat/chat-message.hbs | 23 +++++++++------- 8 files changed, 90 insertions(+), 19 deletions(-) diff --git a/lang/en.json b/lang/en.json index cb020a93..d4c81546 100755 --- a/lang/en.json +++ b/lang/en.json @@ -450,7 +450,8 @@ }, "DaggerheartMenu": { "title": "GM Tools", - "refreshFeatures": "Refresh Features" + "refreshFeatures": "Refresh Features", + "fallingAndCollision": "Falling And Collision Damage" }, "DeleteConfirmation": { "title": "Delete {type} - {name}", @@ -1155,6 +1156,12 @@ "description": "" } }, + "fallAndCollision": { + "veryClose": { "label": "Very Close", "chatTitle": "Fall Damage: Very Close" }, + "close": { "label": "Close", "chatTitle": "Fall Damage: Close" }, + "far": { "label": "Far", "chatTitle": "Fall Damage: Far" }, + "collision": { "label": "Collision", "chatTitle": "Dangerous Collision" } + }, "FeatureForm": { "label": "Feature Form", "passive": "Passive", diff --git a/module/applications/sheets/api/base-actor.mjs b/module/applications/sheets/api/base-actor.mjs index 4b0fd7d9..4a550d72 100644 --- a/module/applications/sheets/api/base-actor.mjs +++ b/module/applications/sheets/api/base-actor.mjs @@ -228,7 +228,6 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { 'systems/daggerheart/templates/ui/chat/action.hbs', systemData ), - title: game.i18n.localize('DAGGERHEART.ACTIONS.Config.displayInChat'), speaker: cls.getSpeaker(), flags: { daggerheart: { diff --git a/module/applications/sidebar/tabs/daggerheartMenu.mjs b/module/applications/sidebar/tabs/daggerheartMenu.mjs index 26ae484b..86c1b8cb 100644 --- a/module/applications/sidebar/tabs/daggerheartMenu.mjs +++ b/module/applications/sidebar/tabs/daggerheartMenu.mjs @@ -31,7 +31,8 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract }, actions: { selectRefreshable: DaggerheartMenu.#selectRefreshable, - refreshActors: DaggerheartMenu.#refreshActors + refreshActors: DaggerheartMenu.#refreshActors, + createFallCollisionDamage: DaggerheartMenu.#createFallCollisionDamage } }; @@ -50,6 +51,7 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract const context = await super._prepareContext(options); context.refreshables = this.refreshSelections; context.disableRefresh = Object.values(this.refreshSelections).every(x => !x.selected); + context.fallAndCollision = CONFIG.DH.GENERAL.fallAndCollisionDamage; return context; } @@ -71,4 +73,22 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract this.refreshSelections = DaggerheartMenu.defaultRefreshSelections(); this.render(); } + + static async #createFallCollisionDamage(_event, button) { + const data = CONFIG.DH.GENERAL.fallAndCollisionDamage[button.dataset.key]; + const roll = new Roll(data.damageFormula); + await roll.evaluate(); + + /* class BaseRoll needed to get rendered by foundryRoll.hbs */ + const rollJSON = roll.toJSON(); + rollJSON.class = 'BaseRoll'; + + foundry.documents.ChatMessage.implementation.create({ + title: game.i18n.localize(data.chatTitle), + author: game.user.id, + speaker: foundry.documents.ChatMessage.implementation.getSpeaker(), + rolls: [rollJSON], + sound: CONFIG.sounds.dice + }); + } } diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index c15cae56..f3484e43 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -1057,3 +1057,30 @@ export const activeEffectDurations = { label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.custom' } }; + +export const fallAndCollisionDamage = { + veryClose: { + id: 'veryClose', + label: 'DAGGERHEART.CONFIG.fallAndCollision.veryClose.label', + chatTitle: 'DAGGERHEART.CONFIG.fallAndCollision.veryClose.chatTitle', + damageFormula: '1d10 + 3' + }, + close: { + id: 'veryClose', + label: 'DAGGERHEART.CONFIG.fallAndCollision.close.label', + chatTitle: 'DAGGERHEART.CONFIG.fallAndCollision.close.chatTitle', + damageFormula: '1d20 + 5' + }, + far: { + id: 'veryClose', + label: 'DAGGERHEART.CONFIG.fallAndCollision.far.label', + chatTitle: 'DAGGERHEART.CONFIG.fallAndCollision.far.chatTitle', + damageFormula: '1d100 + 15' + }, + collision: { + id: 'veryClose', + label: 'DAGGERHEART.CONFIG.fallAndCollision.collision.label', + chatTitle: 'DAGGERHEART.CONFIG.fallAndCollision.collision.chatTitle', + damageFormula: '1d20 + 5' + } +}; diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 56048a81..d1a618c7 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -197,7 +197,6 @@ export default class DHItem extends foundry.documents.Item { actor: item.parent, speaker: cls.getSpeaker(), system: systemData, - title: game.i18n.localize('DAGGERHEART.ACTIONS.Config.displayInChat'), content: await foundry.applications.handlebars.renderTemplate( 'systems/daggerheart/templates/ui/chat/ability-use.hbs', systemData diff --git a/styles/less/ui/sidebar/daggerheartMenu.less b/styles/less/ui/sidebar/daggerheartMenu.less index 677214d7..88b139c5 100644 --- a/styles/less/ui/sidebar/daggerheartMenu.less +++ b/styles/less/ui/sidebar/daggerheartMenu.less @@ -15,17 +15,17 @@ font-weight: bold; } - .menu-refresh-container { + .menu-options-container { display: flex; flex-direction: column; gap: 8px; - .menu-refresh-inner-container { + .menu-options-inner-container { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; - .experience-chip { + .option-chip { display: flex; align-items: center; border-radius: 5px; diff --git a/templates/sidebar/daggerheart-menu/main.hbs b/templates/sidebar/daggerheart-menu/main.hbs index 4d948409..a1ea8730 100644 --- a/templates/sidebar/daggerheart-menu/main.hbs +++ b/templates/sidebar/daggerheart-menu/main.hbs @@ -4,10 +4,10 @@
    {{localize "DAGGERHEART.APPLICATIONS.DaggerheartMenu.refreshFeatures"}} -
    + +
    + {{localize "DAGGERHEART.APPLICATIONS.DaggerheartMenu.fallingAndCollision"}} + + +
  • diff --git a/templates/ui/chat/chat-message.hbs b/templates/ui/chat/chat-message.hbs index 94527f16..8a334b60 100644 --- a/templates/ui/chat/chat-message.hbs +++ b/templates/ui/chat/chat-message.hbs @@ -4,17 +4,22 @@
    - {{#unless actor.name}} -

    {{author.name}}

    + {{#if message.title}} +

    {{message.title}}

    +
    {{actor.name}} {{#if author.isGM}}(GM){{/if}}
    {{else}} - {{#if (eq message.type 'base')}} -

    {{actor.name}}

    -
    {{author.name}}
    + {{#unless actor.name}} +

    {{author.name}}

    {{else}} -

    {{ifThen message.title message.title alias}}

    -
    {{actor.name}} {{#if author.isGM}}(GM){{/if}}
    - {{/if}} - {{/unless}} + {{#if (eq message.type 'base')}} +

    {{actor.name}}

    +
    {{author.name}}
    + {{else}} +

    {{alias}}

    +
    {{actor.name}} {{#if author.isGM}}(GM){{/if}}
    + {{/if}} + {{/unless}} + {{/if}}