From 77bac647a86f886e811bd71126cc51289be75d33 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:31:54 +0100 Subject: [PATCH 1/7] Fixed typo in levelupViewMode domain card option (#1558) --- module/applications/levelup/levelupViewMode.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/module/applications/levelup/levelupViewMode.mjs b/module/applications/levelup/levelupViewMode.mjs index b3d7c30f..afd7dbc4 100644 --- a/module/applications/levelup/levelupViewMode.mjs +++ b/module/applications/levelup/levelupViewMode.mjs @@ -70,7 +70,10 @@ export default class DhlevelUpViewMode extends HandlebarsApplicationMixin(Applic return checkbox; }); - let label = game.i18n.localize(option.label); + let label = + optionKey === 'domainCard' + ? game.i18n.format(option.label, { maxLevel: tier.levels.end }) + : game.i18n.localize(option.label); return { label: label, checkboxGroups: chunkify(checkboxes, option.minCost, chunkedBoxes => { From cc998bffa7ca04bdf203f6969ebde17cf8c751cb Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Tue, 20 Jan 2026 11:03:52 +0100 Subject: [PATCH 2/7] [Feature] DeathMove Condition Improvement (#1562) * Added DeathMove condition and automated changing to the correct condition depending on the result of death moves * . * Update module/data/settings/Automation.mjs Co-authored-by: Chris Ryan <73275196+chrisryan10@users.noreply.github.com> * Update lang/en.json Co-authored-by: Chris Ryan <73275196+chrisryan10@users.noreply.github.com> * Fixed DefeatedCondition localizations --------- Co-authored-by: Chris Ryan <73275196+chrisryan10@users.noreply.github.com> --- lang/en.json | 10 +++++++++- module/applications/dialogs/deathMove.mjs | 11 +++++++---- module/config/generalConfig.mjs | 6 +++++- module/data/actor/character.mjs | 13 ++++++++++++- module/data/settings/Automation.mjs | 16 +++++++++++----- module/documents/actor.mjs | 16 ++++++++++++++-- templates/settings/automation-settings/rules.hbs | 3 ++- 7 files changed, 60 insertions(+), 15 deletions(-) diff --git a/lang/en.json b/lang/en.json index 69965b9e..dda78410 100755 --- a/lang/en.json +++ b/lang/en.json @@ -973,6 +973,10 @@ "outsideRange": "Outside Range" }, "Condition": { + "deathMove": { + "name": "Death Move", + "description": "The character is about to make a Death Move" + }, "dead": { "name": "Dead", "description": "The character is dead" @@ -2435,7 +2439,11 @@ "overlay": { "label": "Overlay Effect" }, "characterDefault": { "label": "Character Default Defeated Status" }, "adversaryDefault": { "label": "Adversary Default Defeated Status" }, - "companionDefault": { "label": "Companion Default Defeated Status" } + "companionDefault": { "label": "Companion Default Defeated Status" }, + "deathMove": { "label": "Death Move" }, + "dead": { "label": "Dead" }, + "defeated": { "label": "Defeated" }, + "unconscious": { "label": "Unconscious" } }, "hopeFear": { "label": "Hope & Fear", diff --git a/module/applications/dialogs/deathMove.mjs b/module/applications/dialogs/deathMove.mjs index 01df6057..d1b9379b 100644 --- a/module/applications/dialogs/deathMove.mjs +++ b/module/applications/dialogs/deathMove.mjs @@ -54,10 +54,9 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV if (!config.roll.fate) return; + let returnMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.avoidScar'); if (config.roll.fate.value <= this.actor.system.levelData.level.current) { - // apply scarring - for now directly apply - later add a button. const newScarAmount = this.actor.system.scars + 1; - await this.actor.update({ system: { scars: newScarAmount @@ -65,13 +64,15 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV }); if (newScarAmount >= this.actor.system.resources.hope.max) { + await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id); return game.i18n.format('DAGGERHEART.UI.Chat.deathMove.journeysEnd', { scars: newScarAmount }); } - return game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.gainScar'); + returnMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.gainScar'); } - return game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.avoidScar'); + await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.unconscious.id); + return returnMessage; } async handleRiskItAll() { @@ -118,6 +119,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV } if (config.roll.result.duality == -1) { + await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id); chatMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.riskItAllFailure'); } @@ -141,6 +143,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV } ]); + await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id); return game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.blazeOfGlory'); } diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 37894644..be1dfce1 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -171,7 +171,7 @@ export const defeatedConditions = () => { acc[key] = { ...choice, img: defeated[`${choice.id}Icon`], - description: `DAGGERHEART.CONFIG.Condition.${choice.id}.description` + description: game.i18n.localize(`DAGGERHEART.CONFIG.Condition.${choice.id}.description`) }; return acc; @@ -179,6 +179,10 @@ export const defeatedConditions = () => { }; export const defeatedConditionChoices = { + deathMove: { + id: 'deathMove', + name: 'DAGGERHEART.CONFIG.Condition.deathMove.name' + }, defeated: { id: 'defeated', name: 'DAGGERHEART.CONFIG.Condition.defeated.name' diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index a7f99ca8..12396384 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -549,7 +549,18 @@ export default class DhCharacter extends BaseDataActor { } get deathMoveViable() { - return this.resources.hitPoints.max > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.max; + const { characterDefault } = game.settings.get( + CONFIG.DH.id, + CONFIG.DH.SETTINGS.gameSettings.Automation + ).defeated; + const deathMoveOutcomeStatuses = Object.keys(CONFIG.DH.GENERAL.defeatedConditionChoices).filter( + key => key !== characterDefault + ); + const deathMoveNotResolved = this.parent.statuses.every(status => !deathMoveOutcomeStatuses.includes(status)); + + const allHitPointsMarked = + this.resources.hitPoints.max > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.max; + return deathMoveNotResolved && allHitPointsMarked; } get armorApplicableDamageTypes() { diff --git a/module/data/settings/Automation.mjs b/module/data/settings/Automation.mjs index bff0bae9..436f0eb7 100644 --- a/module/data/settings/Automation.mjs +++ b/module/data/settings/Automation.mjs @@ -58,7 +58,7 @@ export default class DhAutomation extends foundry.abstract.DataModel { defeated: new fields.SchemaField({ enabled: new fields.BooleanField({ required: true, - initial: false, + initial: true, label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.enabled.label' }), overlay: new fields.BooleanField({ @@ -69,7 +69,7 @@ export default class DhAutomation extends foundry.abstract.DataModel { characterDefault: new fields.StringField({ required: true, choices: CONFIG.DH.GENERAL.defeatedConditionChoices, - initial: CONFIG.DH.GENERAL.defeatedConditionChoices.unconscious.id, + initial: CONFIG.DH.GENERAL.defeatedConditionChoices.deathMove.id, label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.characterDefault.label' }), adversaryDefault: new fields.StringField({ @@ -84,23 +84,29 @@ export default class DhAutomation extends foundry.abstract.DataModel { initial: CONFIG.DH.GENERAL.defeatedConditionChoices.defeated.id, label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.companionDefault.label' }), + deathMoveIcon: new fields.FilePathField({ + initial: 'icons/magic/life/heart-cross-purple-orange.webp', + categories: ['IMAGE'], + base64: false, + label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.deathMove.label' + }), deadIcon: new fields.FilePathField({ initial: 'icons/magic/death/grave-tombstone-glow-teal.webp', categories: ['IMAGE'], base64: false, - label: 'Dead' + label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.dead.label' }), defeatedIcon: new fields.FilePathField({ initial: 'icons/magic/control/fear-fright-mask-orange.webp', categories: ['IMAGE'], base64: false, - label: 'Defeated' + label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.defeated.label' }), unconsciousIcon: new fields.FilePathField({ initial: 'icons/magic/control/sleep-bubble-purple.webp', categories: ['IMAGE'], base64: false, - label: 'Unconcious' + label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.unconscious.label' }) }), roll: new fields.SchemaField({ diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index cec1a24d..d76d7447 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -849,8 +849,8 @@ export default class DhpActor extends Actor { async toggleDefeated(defeatedState) { const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated; - const { unconscious, defeated, dead } = CONFIG.DH.GENERAL.conditions(); - const defeatedConditions = new Set([unconscious.id, defeated.id, dead.id]); + const { deathMove, unconscious, defeated, dead } = CONFIG.DH.GENERAL.conditions(); + const defeatedConditions = new Set([deathMove.id, unconscious.id, defeated.id, dead.id]); if (!defeatedState) { for (let defeatedId of defeatedConditions) { await this.toggleStatusEffect(defeatedId, { overlay: settings.overlay, active: defeatedState }); @@ -864,6 +864,18 @@ export default class DhpActor extends Actor { } } + async setDeathMoveDefeated(defeatedIconId) { + const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated; + const actorDefault = settings[`${this.type}Default`]; + if (!settings.enabled || !settings.enabled || !actorDefault || actorDefault === defeatedIconId) return; + + for (let defeatedId of Object.keys(CONFIG.DH.GENERAL.defeatedConditionChoices)) { + await this.toggleStatusEffect(defeatedId, { overlay: settings.overlay, active: false }); + } + + if (defeatedIconId) await this.toggleStatusEffect(defeatedIconId, { overlay: settings.overlay, active: true }); + } + queueScrollText(scrollingTextData) { this.#scrollTextQueue.push(...scrollingTextData.map(data => () => createScrollText(this, data))); if (!this.#scrollTextInterval) { diff --git a/templates/settings/automation-settings/rules.hbs b/templates/settings/automation-settings/rules.hbs index a12c2999..24f0b262 100644 --- a/templates/settings/automation-settings/rules.hbs +++ b/templates/settings/automation-settings/rules.hbs @@ -9,11 +9,12 @@ {{formGroup settingFields.schema.fields.defeated.fields.enabled value=settingFields._source.defeated.enabled localize=true}} - {{formGroup settingFields.schema.fields.defeated.fields.overlay value=settingFields._source.defeated.overlay localize=true}} + {{formGroup settingFields.schema.fields.defeated.fields.overlay value=settingFields._source.defeated.overlay localize=true}} {{formGroup settingFields.schema.fields.defeated.fields.characterDefault value=settingFields._source.defeated.characterDefault labelAttr="name" localize=true}} {{formGroup settingFields.schema.fields.defeated.fields.adversaryDefault value=settingFields._source.defeated.adversaryDefault labelAttr="name" localize=true}} {{formGroup settingFields.schema.fields.defeated.fields.companionDefault value=settingFields._source.defeated.companionDefault labelAttr="name" localize=true}} + {{formGroup settingFields.schema.fields.defeated.fields.deathMoveIcon value=settingFields._source.defeated.deathMoveIcon localize=true}} {{formGroup settingFields.schema.fields.defeated.fields.deadIcon value=settingFields._source.defeated.deadIcon localize=true}} {{formGroup settingFields.schema.fields.defeated.fields.defeatedIcon value=settingFields._source.defeated.defeatedIcon localize=true}} {{formGroup settingFields.schema.fields.defeated.fields.unconsciousIcon value=settingFields._source.defeated.unconsciousIcon localize=true}} From 3725fc29ef8c8d23f364c03d6d070a4e13415614 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Wed, 21 Jan 2026 02:52:07 +0100 Subject: [PATCH 3/7] [Feature] Seaborne Improvement (#1553) * Added a max to KnowTheTide. Added a onFear trigger to increase the resource * . * Added a notification message when KnowTheTide gains a token --- lang/en.json | 3 +- module/data/registeredTriggers.mjs | 1 + ...eature_Know_the_Tide_07x6Qe6qMzDw2xN4.json | 42 +++++++++++++++++-- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/lang/en.json b/lang/en.json index dda78410..37cd627e 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2876,7 +2876,8 @@ "documentIsMissing": "The {documentType} is missing from the world.", "tokenActorMissing": "{name} is missing an Actor", "tokenActorsMissing": "[{names}] missing Actors", - "domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used" + "domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used", + "knowTheTide": "Know The Tide gained a token" }, "Sidebar": { "actorDirectory": { diff --git a/module/data/registeredTriggers.mjs b/module/data/registeredTriggers.mjs index 8a100585..3fd9f82c 100644 --- a/module/data/registeredTriggers.mjs +++ b/module/data/registeredTriggers.mjs @@ -20,6 +20,7 @@ export default class RegisteredTriggers extends Map { } registerItemTriggers(item, registerOverride) { + if (!item.actor || !item._stats.createdTime) return; for (const action of item.system.actions ?? []) { if (!action.actor) continue; diff --git a/src/packs/communities/feature_Know_the_Tide_07x6Qe6qMzDw2xN4.json b/src/packs/communities/feature_Know_the_Tide_07x6Qe6qMzDw2xN4.json index 069fe6ba..41f11a74 100644 --- a/src/packs/communities/feature_Know_the_Tide_07x6Qe6qMzDw2xN4.json +++ b/src/packs/communities/feature_Know_the_Tide_07x6Qe6qMzDw2xN4.json @@ -9,13 +9,47 @@ "resource": { "type": "simple", "value": 0, - "max": "", - "icon": "", - "recovery": null, + "max": "@system.levelData.level.current", + "icon": "fa-solid fa-water", + "recovery": "session", "diceStates": {}, "dieFaces": "d4" }, - "actions": {}, + "actions": { + "tFlus34KotJjHfTe": { + "type": "effect", + "_id": "tFlus34KotJjHfTe", + "systemPath": "actions", + "baseAction": false, + "description": "", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "triggers": [ + { + "trigger": "fearRoll", + "triggeringActorType": "self", + "command": "const { max, value } = this.item.system.resource;\nconst maxValue = actor.system.levelData.level.current;\nconst afterUpdate = value+1;\nif (afterUpdate > maxValue) return;\n\nui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.knowTheTide'));\nreturn { updates: [{\n key: 'resource',\n itemId: this.item.id,\n target: this.item,\n value: 1,\n}]};" + } + ], + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "effects": [], + "target": { + "type": "any", + "amount": null + }, + "name": "Know The Tide", + "range": "" + } + }, "originItemType": null, "subType": null, "originId": null, From 2aba7cf9213e41e970c31e4b3c0305c3217b0d17 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Wed, 21 Jan 2026 02:56:47 +0100 Subject: [PATCH 4/7] Changed to use a dialog to choose which parts are kept when reseting (#1557) --- lang/en.json | 7 ++ module/applications/dialogs/_module.mjs | 1 + .../dialogs/characterResetDialog.mjs | 105 ++++++++++++++++++ .../applications/sheets/actors/character.mjs | 26 +---- styles/less/dialog/character-reset/sheet.less | 27 +++++ styles/less/dialog/index.less | 2 + templates/dialogs/characterReset.hbs | 33 ++++++ 7 files changed, 179 insertions(+), 22 deletions(-) create mode 100644 module/applications/dialogs/characterResetDialog.mjs create mode 100644 styles/less/dialog/character-reset/sheet.less create mode 100644 templates/dialogs/characterReset.hbs diff --git a/lang/en.json b/lang/en.json index 37cd627e..720a08c5 100755 --- a/lang/en.json +++ b/lang/en.json @@ -330,6 +330,12 @@ "title": "{actor} - Character Setup", "traitIncreases": "Trait Increases" }, + "CharacterReset": { + "title": "Reset Character", + "alwaysDeleteSection": "Deleted Data", + "optionalDeleteSection": "Optional Data", + "headerTitle": "Select which data you'd like to keep" + }, "CombatTracker": { "combatStarted": "Active", "giveSpotlight": "Give The Spotlight", @@ -2214,6 +2220,7 @@ "single": "Player", "plurial": "Players" }, + "portrait": "Portrait", "proficiency": "Proficiency", "quantity": "Quantity", "range": "Range", diff --git a/module/applications/dialogs/_module.mjs b/module/applications/dialogs/_module.mjs index d43045e6..4eda8579 100644 --- a/module/applications/dialogs/_module.mjs +++ b/module/applications/dialogs/_module.mjs @@ -1,5 +1,6 @@ export { default as AttributionDialog } from './attributionDialog.mjs'; export { default as BeastformDialog } from './beastformDialog.mjs'; +export { default as CharacterResetDialog } from './characterResetDialog.mjs'; export { default as d20RollDialog } from './d20RollDialog.mjs'; export { default as DamageDialog } from './damageDialog.mjs'; export { default as DamageReductionDialog } from './damageReductionDialog.mjs'; diff --git a/module/applications/dialogs/characterResetDialog.mjs b/module/applications/dialogs/characterResetDialog.mjs new file mode 100644 index 00000000..1f3f3d5a --- /dev/null +++ b/module/applications/dialogs/characterResetDialog.mjs @@ -0,0 +1,105 @@ +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; + +export default class CharacterResetDialog extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(actor, options = {}) { + super(options); + + this.actor = actor; + this.data = { + delete: { + class: { keep: false, label: 'TYPES.Item.class' }, + subclass: { keep: false, label: 'TYPES.Item.subclass' }, + ancestry: { keep: false, label: 'TYPES.Item.ancestry' }, + community: { keep: false, label: 'TYPES.Item.community' } + }, + optional: { + portrait: { keep: true, label: 'DAGGERHEART.GENERAL.portrait' }, + name: { keep: true, label: 'Name' }, + biography: { keep: true, label: 'DAGGERHEART.GENERAL.Tabs.biography' }, + inventory: { keep: false, label: 'DAGGERHEART.GENERAL.inventory' } + } + }; + } + + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'character-reset'], + window: { + icon: 'fa-solid fa-arrow-rotate-left', + title: 'DAGGERHEART.APPLICATIONS.CharacterReset.title' + }, + actions: { + finishSelection: this.#finishSelection + }, + form: { + handler: this.updateData, + submitOnChange: true, + submitOnClose: false + } + }; + + /** @override */ + static PARTS = { + resourceDice: { + id: 'resourceDice', + template: 'systems/daggerheart/templates/dialogs/characterReset.hbs' + } + }; + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.data = this.data; + + return context; + } + + static async updateData(event, _, formData) { + const { data } = foundry.utils.expandObject(formData.object); + + this.data = foundry.utils.mergeObject(this.data, data); + this.render(); + } + + static getUpdateData() { + const update = {}; + if (!this.data.optional.portrait) update.if(!this.data.optional.biography); + + if (!this.data.optional.inventory) return update; + } + + static async #finishSelection() { + const update = {}; + if (!this.data.optional.name.keep) { + const defaultName = game.system.api.documents.DhpActor.defaultName({ type: 'character' }); + foundry.utils.setProperty(update, 'name', defaultName); + foundry.utils.setProperty(update, 'prototypeToken.name', defaultName); + } + + if (!this.data.optional.portrait.keep) { + foundry.utils.setProperty(update, 'img', this.actor.schema.fields.img.initial(this.actor)); + foundry.utils.setProperty(update, 'prototypeToken.==texture', {}); + foundry.utils.setProperty(update, 'prototypeToken.==ring', {}); + } + + if (this.data.optional.biography.keep) + foundry.utils.setProperty(update, 'system.biography', this.actor.system.biography); + + if (this.data.optional.inventory.keep) foundry.utils.setProperty(update, 'system.gold', this.actor.system.gold); + + const { system, ...rest } = update; + await this.actor.update({ + ...rest, + '==system': system ?? {} + }); + + const inventoryItemTypes = ['weapon', 'armor', 'consumable', 'loot']; + await this.actor.deleteEmbeddedDocuments( + 'Item', + this.actor.items + .filter(x => !inventoryItemTypes.includes(x.type) || !this.data.optional.inventory.keep) + .map(x => x.id) + ); + + this.close(); + } +} diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 5c6bac3a..4ecaeb06 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -669,26 +669,7 @@ export default class CharacterSheet extends DHBaseActorSheet { * Resets the character data and removes all embedded documents. */ static async #resetCharacter() { - const confirmed = await foundry.applications.api.DialogV2.confirm({ - window: { - title: game.i18n.localize('DAGGERHEART.ACTORS.Character.resetCharacterConfirmationTitle') - }, - content: game.i18n.localize('DAGGERHEART.ACTORS.Character.resetCharacterConfirmationContent') - }); - - if (!confirmed) return; - - await this.document.update({ - '==system': {} - }); - await this.document.deleteEmbeddedDocuments( - 'Item', - this.document.items.map(x => x.id) - ); - await this.document.deleteEmbeddedDocuments( - 'ActiveEffect', - this.document.effects.map(x => x.id) - ); + new game.system.api.applications.dialogs.CharacterResetDialog(this.document).render({ force: true }); } /** @@ -753,8 +734,9 @@ export default class CharacterSheet extends DHBaseActorSheet { if (!result) return; /* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */ - const costResources = result.costs?.filter(x => x.enabled) - .map(cost => ({ ...cost, value: -cost.value, total: -cost.total })) || {}; + const costResources = + result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value, total: -cost.total })) || + {}; config.resourceUpdates.addResources(costResources); await config.resourceUpdates.updateResources(); } diff --git a/styles/less/dialog/character-reset/sheet.less b/styles/less/dialog/character-reset/sheet.less new file mode 100644 index 00000000..44312a3e --- /dev/null +++ b/styles/less/dialog/character-reset/sheet.less @@ -0,0 +1,27 @@ +.daggerheart.dh-style.dialog.views.character-reset { + .character-reset-container { + display: flex; + flex-direction: column; + gap: 8px; + + legend { + padding: 0 4px; + } + + .character-reset-header { + font-size: var(--font-size-18); + text-align: center; + } + + .reset-data-container { + display: grid; + grid-template-columns: 3fr 2fr; + align-items: center; + gap: 4px; + + label { + font-weight: bold; + } + } + } +} diff --git a/styles/less/dialog/index.less b/styles/less/dialog/index.less index 01a3f954..733cdd1c 100644 --- a/styles/less/dialog/index.less +++ b/styles/less/dialog/index.less @@ -41,3 +41,5 @@ @import './settings/change-currency-icon.less'; @import './risk-it-all/sheet.less'; + +@import './character-reset/sheet.less'; diff --git a/templates/dialogs/characterReset.hbs b/templates/dialogs/characterReset.hbs new file mode 100644 index 00000000..298826e5 --- /dev/null +++ b/templates/dialogs/characterReset.hbs @@ -0,0 +1,33 @@ +
+
+
{{localize "DAGGERHEART.APPLICATIONS.CharacterReset.headerTitle"}}
+ +
+ {{localize "DAGGERHEART.APPLICATIONS.CharacterReset.alwaysDeleteSection"}} + +
+ {{#each this.data.delete as | data key|}} +
+ + +
+ {{/each}} +
+
+ +
+ {{localize "DAGGERHEART.APPLICATIONS.CharacterReset.optionalDeleteSection"}} + +
+ {{#each this.data.optional as | data key|}} +
+ + +
+ {{/each}} +
+
+ + +
+
\ No newline at end of file From c90875fa7cb14553fd223362433daf186af3c11c Mon Sep 17 00:00:00 2001 From: WBHarry Date: Wed, 21 Jan 2026 14:05:39 +0100 Subject: [PATCH 5/7] Changed ResetDialog to have all optional sections initially kept --- module/applications/dialogs/characterResetDialog.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/applications/dialogs/characterResetDialog.mjs b/module/applications/dialogs/characterResetDialog.mjs index 1f3f3d5a..0836af9c 100644 --- a/module/applications/dialogs/characterResetDialog.mjs +++ b/module/applications/dialogs/characterResetDialog.mjs @@ -16,7 +16,7 @@ export default class CharacterResetDialog extends HandlebarsApplicationMixin(App portrait: { keep: true, label: 'DAGGERHEART.GENERAL.portrait' }, name: { keep: true, label: 'Name' }, biography: { keep: true, label: 'DAGGERHEART.GENERAL.Tabs.biography' }, - inventory: { keep: false, label: 'DAGGERHEART.GENERAL.inventory' } + inventory: { keep: true, label: 'DAGGERHEART.GENERAL.inventory' } } }; } From cbd268ea1f51ab59a1cede31fb28c6f91d3c112c Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sat, 24 Jan 2026 11:10:30 +0100 Subject: [PATCH 6/7] [Feature] DR Command Resources (#1572) * Dr chatcommand and buttons now grant resources via automation by default. Optionally turned off via parameter noResources=true * . --- daggerheart.mjs | 4 +++- module/dice/dualityRoll.mjs | 2 +- module/enrichers/DualityRollEnricher.mjs | 16 ++++++++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/daggerheart.mjs b/daggerheart.mjs index 3abcd210..c3a5c348 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -296,6 +296,7 @@ Hooks.on('chatMessage', (_, message) => { ? CONFIG.DH.ACTIONS.advantageState.disadvantage.value : undefined; const difficulty = rollCommand.difficulty; + const grantResources = Boolean(rollCommand.grantResources); const target = getCommandTarget({ allowNull: true }); const title = traitValue @@ -312,7 +313,8 @@ Hooks.on('chatMessage', (_, message) => { title, label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'), actionType: null, - advantage + advantage, + grantResources }); return false; } diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index aaca7400..30b569ac 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -261,7 +261,7 @@ export default class DualityRoll extends D20Roll { } static async handleTriggers(roll, config) { - if (!config.source?.actor) return; + if (!config.source?.actor || config.skips?.triggers) return; const updates = []; const dualityUpdates = await game.system.registeredTriggers.runTrigger( diff --git a/module/enrichers/DualityRollEnricher.mjs b/module/enrichers/DualityRollEnricher.mjs index 536847f7..67728a37 100644 --- a/module/enrichers/DualityRollEnricher.mjs +++ b/module/enrichers/DualityRollEnricher.mjs @@ -47,6 +47,7 @@ function getDualityMessage(roll, flavor) { ${roll?.trait && abilities[roll.trait] ? `data-trait="${roll.trait}"` : ''} ${roll?.advantage ? 'data-advantage="true"' : ''} ${roll?.disadvantage ? 'data-disadvantage="true"' : ''} + ${roll?.grantResources ? 'data-grant-resources="true"' : ''} > ${roll?.reaction ? '' : ''} ${label} @@ -63,7 +64,8 @@ export const renderDualityButton = async event => { traitValue = button.dataset.trait?.toLowerCase(), target = getCommandTarget({ allowNull: true }), difficulty = button.dataset.difficulty, - advantage = button.dataset.advantage ? Number(button.dataset.advantage) : undefined; + advantage = button.dataset.advantage ? Number(button.dataset.advantage) : undefined, + grantResources = Boolean(button.dataset?.grantResources); await enrichedDualityRoll( { @@ -73,14 +75,15 @@ export const renderDualityButton = async event => { difficulty, title: button.dataset.title, label: button.dataset.label, - advantage + advantage, + grantResources }, event ); }; export const enrichedDualityRoll = async ( - { reaction, traitValue, target, difficulty, title, label, advantage }, + { reaction, traitValue, target, difficulty, title, label, advantage, grantResources }, event ) => { const config = { @@ -93,12 +96,17 @@ export const enrichedDualityRoll = async ( advantage, type: reaction ? 'reaction' : null }, + skips: { + resources: !grantResources, + triggers: !grantResources + }, type: 'trait', hasRoll: true }; if (target) { - await target.diceRoll(config); + const result = await target.diceRoll(config); + result.resourceUpdates.updateResources(); } else { // For no target, call DualityRoll directly with basic data config.data = { experiences: {}, traits: {}, rules: {} }; From bdb89973248dc0001814a3e393697c850c528446 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sat, 24 Jan 2026 11:15:07 +0100 Subject: [PATCH 7/7] [Feature] 1543 - SceneEnvironment Trigger Registration (#1564) * Added trigger Registration/Unregistration for scene environments * Fixed so that saving throw damage mitigation works again (#1555) * . (#1563) * Fixed DowntimeMoves and ItemFeatures reset functions (#1568) * Fixed an error where a player having their token initially selected caused an error in effectsDisplay.mjs (#1569) * [Fix] Damage Rerolls (#1566) * Fixed so that damage rerolls work again * Set default data for a roll instead and fix title (#1570) * Set default data for a roll instead and fix title * Ensure same options object is used --------- Co-authored-by: Carlos Fernandez * Fixed when users drag in compendium environments to the sceneEnvironments (#1573) * . --------- Co-authored-by: Carlos Fernandez --- module/data/registeredTriggers.mjs | 34 ++++++++++++++++++++---------- module/documents/scene.mjs | 21 ++++++++++++++++++ 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/module/data/registeredTriggers.mjs b/module/data/registeredTriggers.mjs index 3fd9f82c..ee4f3b49 100644 --- a/module/data/registeredTriggers.mjs +++ b/module/data/registeredTriggers.mjs @@ -72,10 +72,21 @@ export default class RegisteredTriggers extends Map { } } + unregisterSceneEnvironmentTriggers(flagSystemData) { + const sceneData = new game.system.api.data.scenes.DHScene(flagSystemData); + for (const environment of sceneData.sceneEnvironments) { + if (environment.pack) continue; + this.unregisterItemTriggers(environment.system.features); + } + } + unregisterSceneTriggers(scene) { + this.unregisterSceneEnvironmentTriggers(scene.flags.daggerheart); + for (const triggerKey of Object.keys(CONFIG.DH.TRIGGER.triggers)) { const existingTrigger = this.get(triggerKey); if (!existingTrigger) continue; + const filtered = new Map(); for (const [uuid, data] of existingTrigger.entries()) { if (!uuid.startsWith(scene.uuid)) filtered.set(uuid, data); @@ -84,14 +95,17 @@ export default class RegisteredTriggers extends Map { } } + registerSceneEnvironmentTriggers(flagSystemData) { + const sceneData = new game.system.api.data.scenes.DHScene(flagSystemData); + for (const environment of sceneData.sceneEnvironments) { + for (const feature of environment.system.features) { + if (feature) this.registerItemTriggers(feature, true); + } + } + } + registerSceneTriggers(scene) { - /* TODO: Finish sceneEnvironment registration and unreg */ - // const systemData = new game.system.api.data.scenes.DHScene(scene.flags.daggerheart); - // for (const environment of systemData.sceneEnvironments) { - // for (const feature of environment.system.features) { - // if(feature) this.registerItemTriggers(feature, true); - // } - // } + this.registerSceneEnvironmentTriggers(scene.flags.daggerheart); for (const actor of scene.tokens.filter(x => x.actor).map(x => x.actor)) { if (actor.prototypeToken.actorLink) continue; @@ -108,13 +122,11 @@ export default class RegisteredTriggers extends Map { if (!triggerSettings.enabled) return updates; const dualityTrigger = this.get(trigger); - if (dualityTrigger) { - const tokenBoundActors = ['adversary', 'environment']; - const triggerActors = ['character', ...tokenBoundActors]; + if (dualityTrigger?.size) { + const triggerActors = ['character', 'adversary', 'environment']; for (let [itemUuid, { actor: actorUuid, triggeringActorType, commands }] of dualityTrigger.entries()) { const actor = await foundry.utils.fromUuid(actorUuid); if (!actor || !triggerActors.includes(actor.type)) continue; - if (tokenBoundActors.includes(actor.type) && !actor.getActiveTokens().length) continue; const triggerData = CONFIG.DH.TRIGGER.triggers[trigger]; if (triggerData.usesActor && triggeringActorType !== 'any') { diff --git a/module/documents/scene.mjs b/module/documents/scene.mjs index 7f880b1d..9e2a3f5b 100644 --- a/module/documents/scene.mjs +++ b/module/documents/scene.mjs @@ -51,6 +51,27 @@ export default class DhScene extends Scene { } } + async _preUpdate(changes, options, user) { + const allowed = await super._preUpdate(changes, options, user); + if (allowed === false) return false; + + if (changes.flags?.daggerheart) { + if (this._source.flags.daggerheart) { + const unregisterTriggerData = this._source.flags.daggerheart.sceneEnvironments.reduce( + (acc, env) => { + if (!changes.flags.daggerheart.sceneEnvironments.includes(env)) acc.sceneEnvironments.push(env); + + return acc; + }, + { ...this._source.flags.daggerheart, sceneEnvironments: [] } + ); + game.system.registeredTriggers.unregisterSceneEnvironmentTriggers(unregisterTriggerData); + } + + game.system.registeredTriggers.registerSceneEnvironmentTriggers(changes.flags.daggerheart); + } + } + _onDelete(options, userId) { super._onDelete(options, userId);