From 27b7758f7d8dc3c2c2203272845154d65093dc34 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Tue, 13 Jan 2026 01:15:00 +0100 Subject: [PATCH 01/48] [Feature] Roll Effect Toggles (#1510) * Initial * . * . * Update module/dice/dhRoll.mjs Co-authored-by: Carlos Fernandez --------- Co-authored-by: Carlos Fernandez --- module/applications/dialogs/d20RollDialog.mjs | 10 ++++ module/applications/dialogs/damageDialog.mjs | 10 ++++ .../applications/sheets/actors/character.mjs | 27 +++++---- module/data/action/baseAction.mjs | 12 ++++ module/dice/d20Roll.mjs | 48 +++++++++++++-- module/dice/damageRoll.mjs | 24 +++++++- module/dice/dhRoll.mjs | 59 +++++++++++++++---- module/dice/dualityRoll.mjs | 28 +++++++++ module/documents/activeEffect.mjs | 26 ++++---- module/documents/chatMessage.mjs | 5 +- styles/less/global/dialog.less | 29 +++++++++ .../dialogs/dice-roll/damageSelection.hbs | 14 +++++ templates/dialogs/dice-roll/rollSelection.hbs | 13 ++++ 13 files changed, 266 insertions(+), 39 deletions(-) diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index 34ca02cd..d8306923 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -10,6 +10,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio this.config = config; this.config.experiences = []; this.reactionOverride = config.actionType === 'reaction'; + this.selectedEffects = this.config.bonusEffects; if (config.source?.action) { this.item = config.data.parent.items.get(config.source.item) ?? config.data.parent; @@ -35,6 +36,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio selectExperience: this.selectExperience, toggleReaction: this.toggleReaction, toggleTagTeamRoll: this.toggleTagTeamRoll, + toggleSelectedEffect: this.toggleSelectedEffect, submitRoll: this.submitRoll }, form: { @@ -76,6 +78,9 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio icon })); + context.hasSelectedEffects = Boolean(this.selectedEffects && Object.keys(this.selectedEffects).length); + context.selectedEffects = this.selectedEffects; + this.config.costs ??= []; if (this.config.costs?.length) { const updatedCosts = game.system.api.fields.ActionFields.CostField.calcCosts.call( @@ -208,6 +213,11 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio this.render(); } + static toggleSelectedEffect(_event, button) { + this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected; + this.render(); + } + static async submitRoll() { await this.close({ submitted: true }); } diff --git a/module/applications/dialogs/damageDialog.mjs b/module/applications/dialogs/damageDialog.mjs index fbc584e4..b24570cc 100644 --- a/module/applications/dialogs/damageDialog.mjs +++ b/module/applications/dialogs/damageDialog.mjs @@ -6,6 +6,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application this.roll = roll; this.config = config; + this.selectedEffects = this.config.bonusEffects; } static DEFAULT_OPTIONS = { @@ -20,6 +21,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application icon: 'fa-solid fa-dice' }, actions: { + toggleSelectedEffect: this.toggleSelectedEffect, submitRoll: this.submitRoll }, form: { @@ -57,6 +59,9 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application icon })); context.modifiers = this.config.modifiers; + context.hasSelectedEffects = Boolean(Object.keys(this.selectedEffects).length); + context.selectedEffects = this.selectedEffects; + return context; } @@ -69,6 +74,11 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application this.render(); } + static toggleSelectedEffect(_event, button) { + this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected; + this.render(); + } + static async submitRoll() { await this.close({ submitted: true }); } diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 594269be..0c46f0a1 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -338,15 +338,20 @@ export default class CharacterSheet extends DHBaseActorSheet { } const type = 'effect'; const cls = game.system.api.models.actions.actionsTypes[type]; - const action = new cls({ - ...cls.getSourceConfig(doc.system), - type: type, - chatDisplay: false, - cost: [{ - key: 'stress', - value: doc.system.recallCost - }] - }, { parent: doc.system }); + const action = new cls( + { + ...cls.getSourceConfig(doc.system), + type: type, + chatDisplay: false, + cost: [ + { + key: 'stress', + value: doc.system.recallCost + } + ] + }, + { parent: doc.system } + ); const config = await action.use(event); if (config) { return doc.update({ 'system.inVault': false }); @@ -707,8 +712,10 @@ export default class CharacterSheet extends DHBaseActorSheet { headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { ability: abilityLabel }), + effects: Array.from(await this.document.allApplicableEffects()), roll: { - trait: button.dataset.attribute + trait: button.dataset.attribute, + type: 'trait' }, hasRoll: true, actionType: 'action', diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index 3bf97564..9f9499ac 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -199,6 +199,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel let config = this.prepareConfig(event); if (!config) return; + await this.addEffects(config); + if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return; // Display configuration window if necessary @@ -265,6 +267,16 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel return config; } + /** */ + async addEffects(config) { + let effects = []; + if (this.actor) { + effects = Array.from(await this.actor.allApplicableEffects()); + } + + config.effects = effects; + } + /** * Method used to know if a configuration dialog must be shown or not when there is no roll. * @param {*} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. diff --git a/module/dice/d20Roll.mjs b/module/dice/d20Roll.mjs index 0256f281..f679d725 100644 --- a/module/dice/d20Roll.mjs +++ b/module/dice/d20Roll.mjs @@ -127,15 +127,55 @@ export default class D20Roll extends DHRoll { const modifiers = foundry.utils.deepClone(this.options.roll.baseModifiers) ?? []; modifiers.push( - ...this.getBonus(`roll.${this.options.actionType}`, `${this.options.actionType?.capitalize()} Bonus`) - ); - modifiers.push( - ...this.getBonus(`roll.${this.options.roll.type}`, `${this.options.roll.type?.capitalize()} Bonus`) + ...this.getBonus( + `system.bonuses.roll.${this.options.actionType}`, + `${this.options.actionType?.capitalize()} Bonus` + ) ); + if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.attack.id) { + modifiers.push( + ...this.getBonus( + `system.bonuses.roll.${this.options.roll.type}`, + `${this.options.roll.type?.capitalize()} Bonus` + ) + ); + } + + if ( + this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.attack.id || + (this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id && this.options.hasDamage) + ) { + modifiers.push( + ...this.getBonus(`system.bonuses.roll.attack`, `${this.options.roll.type?.capitalize()} Bonus`) + ); + } + return modifiers; } + getActionChangeKeys() { + const changeKeys = new Set([`system.bonuses.roll.${this.options.actionType}`]); + + if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.attack.id) { + changeKeys.add(`system.bonuses.roll.${this.options.roll.type}`); + } + + if ( + this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.attack.id || + (this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id && this.options.hasDamage) + ) { + changeKeys.add(`system.bonuses.roll.attack`); + } + + if (this.options.roll.trait && this.data.traits?.[this.options.roll.trait]) { + if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.spellcast.id) + changeKeys.add('system.bonuses.roll.trait'); + } + + return changeKeys; + } + static postEvaluate(roll, config = {}) { const data = super.postEvaluate(roll, config); data.type = config.actionType; diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index c10ee6ff..482d2c41 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -93,7 +93,6 @@ export default class DamageRoll extends DHRoll { type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage'), options = part ?? this.options; - modifiers.push(...this.getBonus(`${type}`, `${type.capitalize()} Bonus`)); if (!this.options.hasHealing) { options.damageTypes?.forEach(t => { modifiers.push(...this.getBonus(`${type}.${t}`, `${t.capitalize()} ${type.capitalize()} Bonus`)); @@ -108,6 +107,29 @@ export default class DamageRoll extends DHRoll { return modifiers; } + getActionChangeKeys() { + const type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage'); + const changeKeys = []; + + for (const roll of this.options.roll) { + for (const damageType of roll.damageTypes) changeKeys.push(`system.bonuses.${type}.${damageType}`); + } + + const item = this.data.parent.items?.get(this.options.source.item); + if (item) { + switch (item.type) { + case 'weapon': + if (!this.options.hasHealing) + ['primaryWeapon', 'secondaryWeapon'].forEach(w => + changeKeys.push(`system.bonuses.damage.${w}`) + ); + break; + } + } + + return changeKeys; + } + constructFormula(config) { this.options.roll.forEach((part, index) => { part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data)); diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index ea24f238..a5ac5091 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -4,6 +4,7 @@ export default class DHRoll extends Roll { baseTerms = []; constructor(formula, data = {}, options = {}) { super(formula, data, options); + options.bonusEffects = this.bonusEffectBuilder(); if (!this.data || !Object.keys(this.data).length) this.data = options.data; } @@ -164,12 +165,18 @@ export default class DHRoll extends Roll { new foundry.dice.terms.OperatorTerm({ operator: '+' }), ...this.constructor.parse(modifier.join(' + '), this.options.data) ]; - } else { + } else if (Number.isNumeric(modifier)) { const numTerm = modifier < 0 ? '-' : '+'; return [ new foundry.dice.terms.OperatorTerm({ operator: numTerm }), new foundry.dice.terms.NumericTerm({ number: Math.abs(modifier) }) ]; + } else { + const numTerm = modifier < 0 ? '-' : '+'; + return [ + new foundry.dice.terms.OperatorTerm({ operator: numTerm }), + ...this.constructor.parse(modifier, this.options.data) + ]; } } @@ -185,18 +192,20 @@ export default class DHRoll extends Roll { } getBonus(path, label) { - const bonus = foundry.utils.getProperty(this.data.bonuses, path), - modifiers = []; - if (bonus?.bonus) - modifiers.push({ - label: label, - value: bonus?.bonus - }); - if (bonus?.dice?.length) - modifiers.push({ - label: label, - value: bonus?.dice - }); + const modifiers = []; + for (const effect of Object.values(this.options.bonusEffects)) { + if (!effect.selected) continue; + for (const change of effect.changes) { + if (!change.key.includes(path)) continue; + const changeValue = game.system.api.documents.DhActiveEffect.getChangeValue( + this.data, + change, + effect.origEffect + ); + modifiers.push({ label: label, value: changeValue }); + } + } + return modifiers; } @@ -235,4 +244,28 @@ export default class DHRoll extends Roll { static temporaryModifierBuilder(config) { return {}; } + + bonusEffectBuilder() { + const changeKeys = this.getActionChangeKeys(); + return ( + this.options.effects?.reduce((acc, effect) => { + if (effect.changes.some(x => changeKeys.some(key => x.key.includes(key)))) { + acc[effect.id] = { + id: effect.id, + name: effect.name, + description: effect.description, + changes: effect.changes, + origEffect: effect, + selected: !effect.disabled + }; + } + + return acc; + }, {}) ?? [] + ); + } + + getActionChangeKeys() { + return []; + } } diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index f15e0b09..d035a61c 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -173,6 +173,34 @@ export default class DualityRoll extends D20Roll { return modifiers; } + getActionChangeKeys() { + const changeKeys = new Set([`system.bonuses.roll.${this.options.actionType}`]); + + if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.attack.id) { + changeKeys.add(`system.bonuses.roll.${this.options.roll.type}`); + } + + if ( + this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.attack.id || + (this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id && this.options.hasDamage) + ) { + changeKeys.add(`system.bonuses.roll.attack`); + } + + if (this.options.roll.trait && this.data.traits?.[this.options.roll.trait]) { + if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.spellcast.id) + changeKeys.add('system.bonuses.roll.trait'); + } + + const weapons = ['primaryWeapon', 'secondaryWeapon']; + weapons.forEach(w => { + if (this.options.source.item && this.options.source.item === this.data[w]?.id) + changeKeys.add(`system.bonuses.roll.${w}`); + }); + + return changeKeys; + } + static async buildEvaluate(roll, config = {}, message = {}) { await super.buildEvaluate(roll, config, message); diff --git a/module/documents/activeEffect.mjs b/module/documents/activeEffect.mjs index 2297ea27..c5abb77c 100644 --- a/module/documents/activeEffect.mjs +++ b/module/documents/activeEffect.mjs @@ -106,23 +106,29 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { /**@inheritdoc*/ static applyField(model, change, field) { - const isOriginTarget = change.value.toLowerCase().includes('origin.@'); + change.value = DhActiveEffect.getChangeValue(model, change, change.effect); + super.applyField(model, change, field); + } + + /** */ + static getChangeValue(model, change, effect) { + let value = change.value; + const isOriginTarget = value.toLowerCase().includes('origin.@'); let parseModel = model; - if (isOriginTarget && change.effect.origin) { - change.value = change.value.replaceAll(/origin\.@/gi, '@'); + if (isOriginTarget && effect.origin) { + value = change.value.replaceAll(/origin\.@/gi, '@'); try { - const effect = foundry.utils.fromUuidSync(change.effect.origin); + const originEffect = foundry.utils.fromUuidSync(effect.origin); const doc = - effect.parent?.parent instanceof game.system.api.documents.DhpActor - ? effect.parent - : effect.parent.parent; + originEffect.parent?.parent instanceof game.system.api.documents.DhpActor + ? originEffect.parent + : originEffect.parent.parent; if (doc) parseModel = doc; } catch (_) {} } - const evalValue = this.effectSafeEval(itemAbleRollParse(change.value, parseModel, change.effect.parent)); - change.value = evalValue ?? change.value; - super.applyField(model, change, field); + const evalValue = this.effectSafeEval(itemAbleRollParse(value, parseModel, effect.parent)); + return evalValue ?? value; } /** diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index 7e313891..d85bcb45 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -157,7 +157,10 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { event.stopPropagation(); const config = foundry.utils.deepClone(this.system); config.event = event; - await this.system.action?.workflow.get('damage')?.execute(config, this._id, true); + if (this.system.action) { + await this.system.action.addEffects(config); + await this.system.action.workflow.get('damage')?.execute(config, this._id, true); + } Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll }); await game.socket.emit(`system.${CONFIG.DH.id}`, { diff --git a/styles/less/global/dialog.less b/styles/less/global/dialog.less index 8c532c2b..a3400700 100644 --- a/styles/less/global/dialog.less +++ b/styles/less/global/dialog.less @@ -67,6 +67,35 @@ } } + .dialog-selection-container { + display: flex; + gap: 10px; + flex-wrap: wrap; + + .selection-chip { + display: flex; + align-items: center; + border-radius: 5px; + width: fit-content; + gap: 5px; + cursor: pointer; + padding: 5px; + background: light-dark(@dark-blue-10, @golden-10); + color: light-dark(@dark-blue, @golden); + + .label { + font-style: normal; + font-weight: 400; + font-size: var(--font-size-14); + line-height: 17px; + } + + &.selected { + background: light-dark(@dark-blue-40, @golden-40); + } + } + } + .standard-form { font-family: @font-body; } diff --git a/templates/dialogs/dice-roll/damageSelection.hbs b/templates/dialogs/dice-roll/damageSelection.hbs index ba542666..c0dbae62 100644 --- a/templates/dialogs/dice-roll/damageSelection.hbs +++ b/templates/dialogs/dice-roll/damageSelection.hbs @@ -2,6 +2,20 @@

{{title}}

+ + {{#if hasSelectedEffects}} +
+ {{localize "DAGGERHEART.GENERAL.Effect.plural"}} + + {{#each selectedEffects as |effect id|}} +
+ + {{effect.name}} +
+ {{/each}} +
+ {{/if}} + {{#each @root.formula}}
{{localize "DAGGERHEART.GENERAL.formula"}}: {{roll.formula}} diff --git a/templates/dialogs/dice-roll/rollSelection.hbs b/templates/dialogs/dice-roll/rollSelection.hbs index c7a9b0f9..e60f4683 100644 --- a/templates/dialogs/dice-roll/rollSelection.hbs +++ b/templates/dialogs/dice-roll/rollSelection.hbs @@ -70,6 +70,19 @@ {{/if}}
+ {{#if hasSelectedEffects}} +
+ {{localize "DAGGERHEART.GENERAL.Effect.plural"}} + + {{#each selectedEffects as |effect id|}} +
+ + {{effect.name}} +
+ {{/each}} +
+ {{/if}} + {{#if experiences.length}}
{{localize "DAGGERHEART.GENERAL.experience.plural"}} From d823501d91e57a2e20e8231dcdb08c354542b5c6 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:32:16 +0100 Subject: [PATCH 02/48] [Feature] Advanced Effect Settings (#1523) * Added VaultActive and LoadoutIgnore * Added DomainTouched support * Naming change * . * Improved labels --- lang/en.json | 6 +++- .../applications/sheets/actors/character.mjs | 28 +++++++++--------- module/data/item/domainCard.mjs | 29 ++++++++++++++++++- module/documents/activeEffect.mjs | 5 +++- module/documents/item.mjs | 12 +++++++- ...nCard_Arcana_Touched_5PvMQKCjrgSxzstn.json | 3 +- ...inCard_Blade_Touched_Gb5bqpFSBiuBxUix.json | 3 +- ...ainCard_Bone_Touched_ON5bvnoQBy0SYc9Y.json | 3 +- ...inCard_Codex_Touched_7Pu83ABdMukTxu3e.json | 3 +- ...mainCard_Get_Back_Up_BFWN2cObMdlk9uVz.json | 2 +- ...inCard_Grace_Touched_KAuNb51AwhD8KEXk.json | 3 +- ...ard_Midnight_Touched_uSyGKVxOJcnp28po.json | 3 +- ...domainCard_Notorious_IqxzvvjZiYbgx21A.json | 3 +- ...ainCard_Sage_Touched_VOSFaQHZbmhMyXwi.json | 3 +- ...nCard_Salvation_Beam_4uAFGp3LxiC07woC.json | 2 +- ...ard_Splendor_Touched_JT5dM3gVL6chDBYU.json | 3 +- ...inCard_Valor_Touched_k1AtYd3lSchIymBr.json | 3 +- .../domainCard_Vitality_sWUlSPOJEaXyQLCj.json | 3 +- .../folders_Level_10_7pKKYgRQAKlQAksV.json | 2 +- .../folders_Level_1_9Xc6KzNyjDtTGZkp.json | 2 +- .../folders_Level_2_o7t2fsAmRxKLoHrO.json | 2 +- .../folders_Level_3_wWL9mV6i2EGX5xHS.json | 2 +- .../folders_Level_4_yalAnCU3SndrYImF.json | 2 +- .../folders_Level_5_Emnx4o1DWGTVKoAg.json | 2 +- .../folders_Level_6_EiP5dLozOFZKIeWN.json | 2 +- .../folders_Level_7_HAGbPLHwm0UozDeG.json | 2 +- .../folders_Level_8_me7ywrVh38j6T8Sm.json | 2 +- .../folders_Level_9_QYdeGsmVYIF34kZR.json | 2 +- .../sheets/items/domainCard/settings.hbs | 6 ++++ 29 files changed, 102 insertions(+), 41 deletions(-) diff --git a/lang/en.json b/lang/en.json index 6060a141..d611b04a 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2320,6 +2320,9 @@ "DomainCard": { "type": "Type", "recallCost": "Recall Cost", + "vaultActive": "Active In Vault", + "loadoutIgnore": "Ignores Loadout Limits", + "domainTouched": "Domain Touched", "foundationTitle": "Foundation", "specializationTitle": "Specialization", "masteryTitle": "Mastery" @@ -2820,7 +2823,8 @@ "noActorOwnership": "You do not have permissions for this character", "documentIsMissing": "The {documentType} is missing from the world.", "tokenActorMissing": "{name} is missing an Actor", - "tokenActorsMissing": "[{names}] missing Actors" + "tokenActorsMissing": "[{names}] missing Actors", + "domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used" }, "Sidebar": { "actorDirectory": { diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 0c46f0a1..51ce5bab 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -33,7 +33,7 @@ export default class CharacterSheet extends DHBaseActorSheet { advanceResourceDie: CharacterSheet.#advanceResourceDie, cancelBeastform: CharacterSheet.#cancelBeastform, useDowntime: this.useDowntime, - viewParty: CharacterSheet.#viewParty, + viewParty: CharacterSheet.#viewParty }, window: { resizable: true, @@ -829,7 +829,7 @@ export default class CharacterSheet extends DHBaseActorSheet { static async #toggleVault(_event, button) { const doc = await getDocFromElement(button); const { available } = this.document.system.loadoutSlot; - if (doc.system.inVault && !available) { + if (doc.system.inVault && !available && !doc.system.loadoutIgnore) { return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.loadoutMaxReached')); } @@ -907,32 +907,32 @@ export default class CharacterSheet extends DHBaseActorSheet { return; } - const buttons = parties.map((p) => { - const button = document.createElement("button"); - button.type = "button"; - button.classList.add("plain"); - const img = document.createElement("img"); + const buttons = parties.map(p => { + const button = document.createElement('button'); + button.type = 'button'; + button.classList.add('plain'); + const img = document.createElement('img'); img.src = p.img; button.append(img); - const name = document.createElement("span"); + const name = document.createElement('span'); name.textContent = p.name; button.append(name); - button.addEventListener("click", () => { + button.addEventListener('click', () => { p.sheet?.render({ force: true }); game.tooltip.dismissLockedTooltips(); }); return button; }); - const html = document.createElement("div"); - html.classList.add("party-list"); + const html = document.createElement('div'); + html.classList.add('party-list'); html.append(...buttons); - + game.tooltip.dismissLockedTooltips(); game.tooltip.activate(target, { html, - locked: true, - }) + locked: true + }); } /** diff --git a/module/data/item/domainCard.mjs b/module/data/item/domainCard.mjs index 92d8828c..327dafce 100644 --- a/module/data/item/domainCard.mjs +++ b/module/data/item/domainCard.mjs @@ -29,7 +29,21 @@ export default class DHDomainCard extends BaseDataItem { required: true, initial: CONFIG.DH.DOMAIN.cardTypes.ability.id }), - inVault: new fields.BooleanField({ initial: false }) + inVault: new fields.BooleanField({ initial: false }), + vaultActive: new fields.BooleanField({ + required: true, + nullable: false, + initial: false + }), + loadoutIgnore: new fields.BooleanField({ + required: true, + nullable: false, + initial: false + }), + domainTouched: new fields.NumberField({ + nullable: true, + initial: null + }) }; } @@ -38,6 +52,19 @@ export default class DHDomainCard extends BaseDataItem { return game.i18n.localize(allDomainData[this.domain].label); } + get isVaultSupressed() { + return this.inVault && !this.vaultActive; + } + + get isDomainTouchedSuppressed() { + if (!this.parent.system.domainTouched || this.parent.parent?.type !== 'character') return false; + + const matchingDomainCards = this.parent.parent.items.filter( + item => !item.system.inVault && item.system.domain === this.parent.system.domain + ).length; + return matchingDomainCards < this.parent.system.domainTouched; + } + /* -------------------------------------------- */ /**@override */ diff --git a/module/documents/activeEffect.mjs b/module/documents/activeEffect.mjs index c5abb77c..5e9b0c3b 100644 --- a/module/documents/activeEffect.mjs +++ b/module/documents/activeEffect.mjs @@ -20,7 +20,10 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { } if (this.parent?.type === 'domainCard') { - return this.parent.system.inVault; + const isVaultSupressed = this.parent.system.isVaultSupressed; + const domainTouchedSupressed = this.parent.system.isDomainTouchedSuppressed; + + return isVaultSupressed || domainTouchedSupressed; } return super.isSuppressed; diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 2c6d68b5..7607658c 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -31,7 +31,7 @@ export default class DHItem extends foundry.documents.Item { static async createDocuments(sources, operation) { // Ensure that items being created are valid to the actor its being added to const actor = operation.parent; - sources = actor?.system?.isItemValid ? sources.filter((s) => actor.system.isItemValid(s)) : sources; + sources = actor?.system?.isItemValid ? sources.filter(s => actor.system.isItemValid(s)) : sources; return super.createDocuments(sources, operation); } @@ -146,6 +146,16 @@ export default class DHItem extends foundry.documents.Item { /* -------------------------------------------- */ async use(event) { + /* DomainCard check. Can be expanded or made neater */ + if (this.system.isDomainTouchedSuppressed) { + return ui.notifications.warn( + game.i18n.format('DAGGERHEART.UI.Notifications.domainTouchRequirement', { + nr: this.domainTouched, + domain: game.i18n.localize(CONFIG.DH.DOMAIN.allDomains()[this.domain].label) + }) + ); + } + const actions = new Set(this.system.actionsList); if (actions?.size) { let action = actions.first(); diff --git a/src/packs/domains/domainCard_Arcana_Touched_5PvMQKCjrgSxzstn.json b/src/packs/domains/domainCard_Arcana_Touched_5PvMQKCjrgSxzstn.json index 80d9797f..556d5074 100644 --- a/src/packs/domains/domainCard_Arcana_Touched_5PvMQKCjrgSxzstn.json +++ b/src/packs/domains/domainCard_Arcana_Touched_5PvMQKCjrgSxzstn.json @@ -54,7 +54,8 @@ "source": "Daggerheart SRD", "page": 120, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "5PvMQKCjrgSxzstn", diff --git a/src/packs/domains/domainCard_Blade_Touched_Gb5bqpFSBiuBxUix.json b/src/packs/domains/domainCard_Blade_Touched_Gb5bqpFSBiuBxUix.json index d2d5dafc..ebb95570 100644 --- a/src/packs/domains/domainCard_Blade_Touched_Gb5bqpFSBiuBxUix.json +++ b/src/packs/domains/domainCard_Blade_Touched_Gb5bqpFSBiuBxUix.json @@ -13,7 +13,8 @@ "source": "Daggerheart SRD", "page": 121, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "Gb5bqpFSBiuBxUix", diff --git a/src/packs/domains/domainCard_Bone_Touched_ON5bvnoQBy0SYc9Y.json b/src/packs/domains/domainCard_Bone_Touched_ON5bvnoQBy0SYc9Y.json index 770ddd63..8880bb07 100644 --- a/src/packs/domains/domainCard_Bone_Touched_ON5bvnoQBy0SYc9Y.json +++ b/src/packs/domains/domainCard_Bone_Touched_ON5bvnoQBy0SYc9Y.json @@ -46,7 +46,8 @@ "source": "Daggerheart SRD", "page": 123, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "ON5bvnoQBy0SYc9Y", diff --git a/src/packs/domains/domainCard_Codex_Touched_7Pu83ABdMukTxu3e.json b/src/packs/domains/domainCard_Codex_Touched_7Pu83ABdMukTxu3e.json index 1e2d5de3..6443ed6a 100644 --- a/src/packs/domains/domainCard_Codex_Touched_7Pu83ABdMukTxu3e.json +++ b/src/packs/domains/domainCard_Codex_Touched_7Pu83ABdMukTxu3e.json @@ -71,7 +71,8 @@ "source": "Daggerheart SRD", "page": 125, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "7Pu83ABdMukTxu3e", diff --git a/src/packs/domains/domainCard_Get_Back_Up_BFWN2cObMdlk9uVz.json b/src/packs/domains/domainCard_Get_Back_Up_BFWN2cObMdlk9uVz.json index ab74e805..571a3fb4 100644 --- a/src/packs/domains/domainCard_Get_Back_Up_BFWN2cObMdlk9uVz.json +++ b/src/packs/domains/domainCard_Get_Back_Up_BFWN2cObMdlk9uVz.json @@ -18,7 +18,7 @@ }, "flags": {}, "_id": "BFWN2cObMdlk9uVz", - "sort": 3400000, + "sort": 3500000, "effects": [ { "name": "Get Back Up", diff --git a/src/packs/domains/domainCard_Grace_Touched_KAuNb51AwhD8KEXk.json b/src/packs/domains/domainCard_Grace_Touched_KAuNb51AwhD8KEXk.json index b87ea24d..346a81f2 100644 --- a/src/packs/domains/domainCard_Grace_Touched_KAuNb51AwhD8KEXk.json +++ b/src/packs/domains/domainCard_Grace_Touched_KAuNb51AwhD8KEXk.json @@ -96,7 +96,8 @@ "source": "Daggerheart SRD", "page": 127, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "KAuNb51AwhD8KEXk", diff --git a/src/packs/domains/domainCard_Midnight_Touched_uSyGKVxOJcnp28po.json b/src/packs/domains/domainCard_Midnight_Touched_uSyGKVxOJcnp28po.json index 3370c30e..10c42418 100644 --- a/src/packs/domains/domainCard_Midnight_Touched_uSyGKVxOJcnp28po.json +++ b/src/packs/domains/domainCard_Midnight_Touched_uSyGKVxOJcnp28po.json @@ -111,7 +111,8 @@ "source": "Daggerheart SRD", "page": 129, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "uSyGKVxOJcnp28po", diff --git a/src/packs/domains/domainCard_Notorious_IqxzvvjZiYbgx21A.json b/src/packs/domains/domainCard_Notorious_IqxzvvjZiYbgx21A.json index dfb581e7..2e5f5ffd 100644 --- a/src/packs/domains/domainCard_Notorious_IqxzvvjZiYbgx21A.json +++ b/src/packs/domains/domainCard_Notorious_IqxzvvjZiYbgx21A.json @@ -44,7 +44,8 @@ "source": "Daggerheart SRD", "page": 127, "artist": "" - } + }, + "loadoutIgnore": true }, "flags": {}, "_id": "IqxzvvjZiYbgx21A", diff --git a/src/packs/domains/domainCard_Sage_Touched_VOSFaQHZbmhMyXwi.json b/src/packs/domains/domainCard_Sage_Touched_VOSFaQHZbmhMyXwi.json index dc9ac3d3..432ff638 100644 --- a/src/packs/domains/domainCard_Sage_Touched_VOSFaQHZbmhMyXwi.json +++ b/src/packs/domains/domainCard_Sage_Touched_VOSFaQHZbmhMyXwi.json @@ -94,7 +94,8 @@ "source": "Daggerheart SRD", "page": 131, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "VOSFaQHZbmhMyXwi", diff --git a/src/packs/domains/domainCard_Salvation_Beam_4uAFGp3LxiC07woC.json b/src/packs/domains/domainCard_Salvation_Beam_4uAFGp3LxiC07woC.json index d637f611..c7aeb02f 100644 --- a/src/packs/domains/domainCard_Salvation_Beam_4uAFGp3LxiC07woC.json +++ b/src/packs/domains/domainCard_Salvation_Beam_4uAFGp3LxiC07woC.json @@ -95,7 +95,7 @@ }, "flags": {}, "_id": "4uAFGp3LxiC07woC", - "sort": 3400000, + "sort": 3500000, "effects": [], "ownership": { "default": 0 diff --git a/src/packs/domains/domainCard_Splendor_Touched_JT5dM3gVL6chDBYU.json b/src/packs/domains/domainCard_Splendor_Touched_JT5dM3gVL6chDBYU.json index 45d0dc96..6b530289 100644 --- a/src/packs/domains/domainCard_Splendor_Touched_JT5dM3gVL6chDBYU.json +++ b/src/packs/domains/domainCard_Splendor_Touched_JT5dM3gVL6chDBYU.json @@ -13,7 +13,8 @@ "source": "Daggerheart SRD", "page": 133, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "JT5dM3gVL6chDBYU", diff --git a/src/packs/domains/domainCard_Valor_Touched_k1AtYd3lSchIymBr.json b/src/packs/domains/domainCard_Valor_Touched_k1AtYd3lSchIymBr.json index 99546d6f..20fe18ea 100644 --- a/src/packs/domains/domainCard_Valor_Touched_k1AtYd3lSchIymBr.json +++ b/src/packs/domains/domainCard_Valor_Touched_k1AtYd3lSchIymBr.json @@ -82,7 +82,8 @@ "source": "Daggerheart SRD", "page": 134, "artist": "" - } + }, + "domainTouched": 4 }, "flags": {}, "_id": "k1AtYd3lSchIymBr", diff --git a/src/packs/domains/domainCard_Vitality_sWUlSPOJEaXyQLCj.json b/src/packs/domains/domainCard_Vitality_sWUlSPOJEaXyQLCj.json index 729aa251..ec47c9f9 100644 --- a/src/packs/domains/domainCard_Vitality_sWUlSPOJEaXyQLCj.json +++ b/src/packs/domains/domainCard_Vitality_sWUlSPOJEaXyQLCj.json @@ -51,7 +51,8 @@ "source": "Daggerheart SRD", "page": 121, "artist": "" - } + }, + "vaultActive": true }, "flags": {}, "_id": "sWUlSPOJEaXyQLCj", diff --git a/src/packs/domains/folders_Level_10_7pKKYgRQAKlQAksV.json b/src/packs/domains/folders_Level_10_7pKKYgRQAKlQAksV.json index 613aa993..126323da 100644 --- a/src/packs/domains/folders_Level_10_7pKKYgRQAKlQAksV.json +++ b/src/packs/domains/folders_Level_10_7pKKYgRQAKlQAksV.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "7pKKYgRQAKlQAksV", "description": "", - "sort": 1000000, + "sort": 950000, "flags": {}, "_key": "!folders!7pKKYgRQAKlQAksV" } diff --git a/src/packs/domains/folders_Level_1_9Xc6KzNyjDtTGZkp.json b/src/packs/domains/folders_Level_1_9Xc6KzNyjDtTGZkp.json index 095ff6fb..2d9c78f9 100644 --- a/src/packs/domains/folders_Level_1_9Xc6KzNyjDtTGZkp.json +++ b/src/packs/domains/folders_Level_1_9Xc6KzNyjDtTGZkp.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "9Xc6KzNyjDtTGZkp", "description": "", - "sort": 100000, + "sort": 700000, "flags": {}, "_key": "!folders!9Xc6KzNyjDtTGZkp" } diff --git a/src/packs/domains/folders_Level_2_o7t2fsAmRxKLoHrO.json b/src/packs/domains/folders_Level_2_o7t2fsAmRxKLoHrO.json index b242e121..68cc5f04 100644 --- a/src/packs/domains/folders_Level_2_o7t2fsAmRxKLoHrO.json +++ b/src/packs/domains/folders_Level_2_o7t2fsAmRxKLoHrO.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "o7t2fsAmRxKLoHrO", "description": "", - "sort": 200000, + "sort": 800000, "flags": {}, "_key": "!folders!o7t2fsAmRxKLoHrO" } diff --git a/src/packs/domains/folders_Level_3_wWL9mV6i2EGX5xHS.json b/src/packs/domains/folders_Level_3_wWL9mV6i2EGX5xHS.json index 3a4b0055..e04c6f09 100644 --- a/src/packs/domains/folders_Level_3_wWL9mV6i2EGX5xHS.json +++ b/src/packs/domains/folders_Level_3_wWL9mV6i2EGX5xHS.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "wWL9mV6i2EGX5xHS", "description": "", - "sort": 300000, + "sort": 850000, "flags": {}, "_key": "!folders!wWL9mV6i2EGX5xHS" } diff --git a/src/packs/domains/folders_Level_4_yalAnCU3SndrYImF.json b/src/packs/domains/folders_Level_4_yalAnCU3SndrYImF.json index ab0ba963..2b70a495 100644 --- a/src/packs/domains/folders_Level_4_yalAnCU3SndrYImF.json +++ b/src/packs/domains/folders_Level_4_yalAnCU3SndrYImF.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "yalAnCU3SndrYImF", "description": "", - "sort": 400000, + "sort": 900000, "flags": {}, "_key": "!folders!yalAnCU3SndrYImF" } diff --git a/src/packs/domains/folders_Level_5_Emnx4o1DWGTVKoAg.json b/src/packs/domains/folders_Level_5_Emnx4o1DWGTVKoAg.json index 0a821a2d..5bde56f3 100644 --- a/src/packs/domains/folders_Level_5_Emnx4o1DWGTVKoAg.json +++ b/src/packs/domains/folders_Level_5_Emnx4o1DWGTVKoAg.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "Emnx4o1DWGTVKoAg", "description": "", - "sort": 500000, + "sort": 901563, "flags": {}, "_key": "!folders!Emnx4o1DWGTVKoAg" } diff --git a/src/packs/domains/folders_Level_6_EiP5dLozOFZKIeWN.json b/src/packs/domains/folders_Level_6_EiP5dLozOFZKIeWN.json index 5a58c052..e20ae6b5 100644 --- a/src/packs/domains/folders_Level_6_EiP5dLozOFZKIeWN.json +++ b/src/packs/domains/folders_Level_6_EiP5dLozOFZKIeWN.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "EiP5dLozOFZKIeWN", "description": "", - "sort": 600000, + "sort": 903125, "flags": {}, "_key": "!folders!EiP5dLozOFZKIeWN" } diff --git a/src/packs/domains/folders_Level_7_HAGbPLHwm0UozDeG.json b/src/packs/domains/folders_Level_7_HAGbPLHwm0UozDeG.json index 233e3756..e53c0e2a 100644 --- a/src/packs/domains/folders_Level_7_HAGbPLHwm0UozDeG.json +++ b/src/packs/domains/folders_Level_7_HAGbPLHwm0UozDeG.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "HAGbPLHwm0UozDeG", "description": "", - "sort": 700000, + "sort": 906250, "flags": {}, "_key": "!folders!HAGbPLHwm0UozDeG" } diff --git a/src/packs/domains/folders_Level_8_me7ywrVh38j6T8Sm.json b/src/packs/domains/folders_Level_8_me7ywrVh38j6T8Sm.json index 2b125f0d..9a0ad8d9 100644 --- a/src/packs/domains/folders_Level_8_me7ywrVh38j6T8Sm.json +++ b/src/packs/domains/folders_Level_8_me7ywrVh38j6T8Sm.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "me7ywrVh38j6T8Sm", "description": "", - "sort": 800000, + "sort": 912500, "flags": {}, "_key": "!folders!me7ywrVh38j6T8Sm" } diff --git a/src/packs/domains/folders_Level_9_QYdeGsmVYIF34kZR.json b/src/packs/domains/folders_Level_9_QYdeGsmVYIF34kZR.json index c7984fb9..3547b169 100644 --- a/src/packs/domains/folders_Level_9_QYdeGsmVYIF34kZR.json +++ b/src/packs/domains/folders_Level_9_QYdeGsmVYIF34kZR.json @@ -6,7 +6,7 @@ "sorting": "a", "_id": "QYdeGsmVYIF34kZR", "description": "", - "sort": 900000, + "sort": 925000, "flags": {}, "_key": "!folders!QYdeGsmVYIF34kZR" } diff --git a/templates/sheets/items/domainCard/settings.hbs b/templates/sheets/items/domainCard/settings.hbs index f3d05a2a..f5781606 100644 --- a/templates/sheets/items/domainCard/settings.hbs +++ b/templates/sheets/items/domainCard/settings.hbs @@ -14,6 +14,12 @@ {{formField systemFields.level value=source.system.level data-dtype="Number"}} {{localize "DAGGERHEART.ITEMS.DomainCard.recallCost"}} {{formField systemFields.recallCost value=source.system.recallCost data-dtype="Number"}} + {{localize "DAGGERHEART.ITEMS.DomainCard.vaultActive"}} + {{formField systemFields.vaultActive value=source.system.vaultActive}} + {{localize "DAGGERHEART.ITEMS.DomainCard.loadoutIgnore"}} + {{formField systemFields.loadoutIgnore value=source.system.loadoutIgnore}} + {{localize "DAGGERHEART.ITEMS.DomainCard.domainTouched"}} + {{formField systemFields.domainTouched value=source.system.domainTouched placeholder=0 }}
{{> "systems/daggerheart/templates/sheets/global/partials/resource-section/resource-section.hbs" }} From d282a815946599f82b7fb3108a3b2d2dd2057fcb Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:36:29 +0100 Subject: [PATCH 03/48] [Feature] Summon Action Implementation (#1514) * Schema definition for DHSummonAction * Will reimplement * HBS idea formed. Need to recheck drag drop implementation * Tried to refine drag drop * drag drop implemented (css tbd) * phase 1 complete * tbd work on summon action type * Improved Schema and now it works * . * Dialog created. Tokens not dragged(tbd). * Bare minimum implementation * Finalized functionality * Cleanup * . * Added optional summon render to chat message * Updated SRD * bugfix: fix title lines not rendering in chat messages * Added summon actions to the easily doable environments in the SRD * Update module/data/fields/action/summonField.mjs Co-authored-by: Carlos Fernandez --------- Co-authored-by: Nikhil Nagarajan Co-authored-by: Murilo Brito Co-authored-by: Carlos Fernandez --- daggerheart.mjs | 5 + lang/en.json | 16 ++- module/applications/dialogs/downtime.mjs | 41 +++---- .../sheets-configs/action-base-config.mjs | 78 ++++++++++++- .../applications/sheets/actors/adversary.mjs | 3 +- module/applications/ui/chatLog.mjs | 2 +- module/canvas/_module.mjs | 1 + module/canvas/placeables/token.mjs | 43 ++++++-- module/canvas/tokens.mjs | 16 +++ module/data/action/baseAction.mjs | 1 - module/data/action/summonAction.mjs | 16 +-- module/data/fields/action/_module.mjs | 1 + module/data/fields/action/summonField.mjs | 89 +++++++++++++++ module/data/fields/actionField.mjs | 3 +- module/documents/_module.mjs | 1 + module/documents/tokenManager.mjs | 104 ++++++++++++++++++ module/systemRegistration/handlebars.mjs | 1 + ...ary_Arch_Necromancer_WPEOIGfclNJxWb87.json | 34 +++--- ...rsary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json | 21 ++-- .../adversary_Dryad_wR7cFKrHvRzbzhBT.json | 34 +++--- ...adversary_Green_Ooze_SHXedd9zZPVfUgUa.json | 31 ++++-- ...versary_Head_Vampire_i2UNbRvgyoSs07M6.json | 36 +++--- ...sary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json | 34 +++--- ...ged_Knife_Lieutenant_aTljstqteGoLpCBq.json | 30 ++++- ...dversary_Petty_Noble_wycLpvebWdUqRhpP.json | 57 ++++------ ...rsary_Pirate_Captain_OROJbjsqagVh7ECV.json | 31 ++++-- .../adversary_Red_Ooze_9rVlbJVrDNn1x7PS.json | 29 +++-- ...ersary_Secret_Keeper_sLAccjvCWfeedbpI.json | 85 +++++++------- ...rsary_Tangle_Bramble_XcAGOSmtCFLT1unN.json | 30 ++++- ...g_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json | 2 +- ...ironment_Chaos_Realm_2Z1mKc65LxNk2PqR.json | 39 +++++-- ...ironment_Cult_Ritual_QAXXiOKBDmCTauHD.json | 21 ++-- ...nt_Divine_Usurpation_4DLYez7VbMCFDAuZ.json | 36 +++--- ...onment_Mountain_Pass_acMu9wJrMZZzLSTJ.json | 30 ++++- ...ronment_Raging_River_t4cdqTfzcqP3H1vJ.json | 31 ++++-- styles/less/sheets/actions/actions.less | 49 +++++++++ .../less/sheets/actors/character/header.less | 2 +- styles/less/sheets/index.less | 2 + styles/less/ui/chat/action.less | 55 +++++++++ styles/less/ui/chat/downtime.less | 3 +- templates/actionTypes/summon.hbs | 50 +++++++++ .../action-settings/configuration.hbs | 2 +- .../action-settings/effect.hbs | 1 + templates/ui/chat/action.hbs | 18 ++- 44 files changed, 902 insertions(+), 312 deletions(-) create mode 100644 module/canvas/tokens.mjs create mode 100644 module/data/fields/action/summonField.mjs create mode 100644 module/documents/tokenManager.mjs create mode 100644 templates/actionTypes/summon.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index 861d16ca..cd4dbc19 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -2,6 +2,7 @@ import { SYSTEM } from './module/config/system.mjs'; import * as applications from './module/applications/_module.mjs'; import * as data from './module/data/_module.mjs'; import * as models from './module/data/_module.mjs'; +import * as canvas from './module/canvas/_module.mjs'; import * as documents from './module/documents/_module.mjs'; import * as dice from './module/dice/_module.mjs'; import * as fields from './module/data/fields/_module.mjs'; @@ -19,6 +20,7 @@ import { import { placeables } from './module/canvas/_module.mjs'; import './node_modules/@yaireo/tagify/dist/tagify.css'; import TemplateManager from './module/documents/templateManager.mjs'; +import TokenManager from './module/documents/tokenManager.mjs'; CONFIG.DH = SYSTEM; CONFIG.TextEditor.enrichers.push(...enricherConfig); @@ -51,6 +53,8 @@ CONFIG.ChatMessage.template = 'systems/daggerheart/templates/ui/chat/chat-messag CONFIG.Canvas.rulerClass = placeables.DhRuler; CONFIG.Canvas.layers.templates.layerClass = placeables.DhTemplateLayer; +CONFIG.Canvas.layers.tokens.layerClass = canvas.DhTokenLayer; + CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate; CONFIG.Scene.documentClass = documents.DhScene; @@ -74,6 +78,7 @@ CONFIG.ui.countdowns = applications.ui.DhCountdowns; CONFIG.ux.ContextMenu = applications.ux.DHContextMenu; CONFIG.ux.TooltipManager = documents.DhTooltipManager; CONFIG.ux.TemplateManager = new TemplateManager(); +CONFIG.ux.TokenManager = new TokenManager(); Hooks.once('init', () => { game.system.api = { diff --git a/lang/en.json b/lang/en.json index d611b04a..66955580 100755 --- a/lang/en.json +++ b/lang/en.json @@ -69,7 +69,11 @@ }, "summon": { "name": "Summon", - "tooltip": "Create tokens in the scene." + "tooltip": "Create tokens in the scene.", + "error": "You do not have permission to summon tokens or there is no active scene.", + "invalidDrop": "You can only drop Actor entities to summon.", + "chatMessageTitle": "Test2", + "chatMessageHeaderTitle": "Summoning" } }, "Config": { @@ -122,6 +126,9 @@ }, "cost": { "stepTooltip": "+{step} per step" + }, + "summon": { + "dropSummonsHere": "Drop Summons Here" } } }, @@ -2205,6 +2212,10 @@ "stress": "Stress", "subclasses": "Subclasses", "success": "Success", + "summon": { + "single": "Summon", + "plural": "Summons" + }, "take": "Take", "Target": { "single": "Target", @@ -2869,7 +2880,8 @@ "deleteItem": "Delete Item", "immune": "Immune", "middleClick": "[Middle Click] Keep tooltip view", - "tokenSize": "The token size used on the canvas" + "tokenSize": "The token size used on the canvas", + "previewTokenHelp": "Left-click to place, right-click to cancel" } } } diff --git a/module/applications/dialogs/downtime.mjs b/module/applications/dialogs/downtime.mjs index f03524f0..9a9a9ddb 100644 --- a/module/applications/dialogs/downtime.mjs +++ b/module/applications/dialogs/downtime.mjs @@ -93,27 +93,29 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV } getRefreshables() { - const actionItems = this.actor.items.filter(x => this.actor.system.isItemAvailable(x)).reduce((acc, x) => { - if (x.system.actions) { - const recoverable = x.system.actions.reduce((acc, action) => { - if (refreshIsAllowed([this.shortrest ? 'shortRest' : 'longRest'], action.uses.recovery)) { - acc.push({ - title: x.name, - name: action.name, - uuid: action.uuid - }); + const actionItems = this.actor.items + .filter(x => this.actor.system.isItemAvailable(x)) + .reduce((acc, x) => { + if (x.system.actions) { + const recoverable = x.system.actions.reduce((acc, action) => { + if (refreshIsAllowed([this.shortrest ? 'shortRest' : 'longRest'], action.uses.recovery)) { + acc.push({ + title: x.name, + name: action.name, + uuid: action.uuid + }); + } + + return acc; + }, []); + + if (recoverable) { + acc.push(...recoverable); } - - return acc; - }, []); - - if (recoverable) { - acc.push(...recoverable); } - } - return acc; - }, []); + return acc; + }, []); const resourceItems = this.actor.items.reduce((acc, x) => { if ( x.system.resource && @@ -189,7 +191,8 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV })); }); }); - const characters = game.actors.filter(x => x.type === 'character') + const characters = game.actors + .filter(x => x.type === 'character') .filter(x => x.testUserPermission(game.user, 'LIMITED')) .filter(x => x.uuid !== this.actor.uuid); diff --git a/module/applications/sheets-configs/action-base-config.mjs b/module/applications/sheets-configs/action-base-config.mjs index 16ebcab5..7051ad2b 100644 --- a/module/applications/sheets-configs/action-base-config.mjs +++ b/module/applications/sheets-configs/action-base-config.mjs @@ -31,6 +31,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) editEffect: this.editEffect, addDamage: this.addDamage, removeDamage: this.removeDamage, + editDoc: this.editDoc, addTrigger: this.addTrigger, removeTrigger: this.removeTrigger, expandTrigger: this.expandTrigger @@ -39,7 +40,8 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) handler: this.updateForm, submitOnChange: true, closeOnSubmit: false - } + }, + dragDrop: [{ dragSelector: null, dropSelector: '#summon-drop-zone', handlers: ['_onDrop'] }] }; static PARTS = { @@ -101,7 +103,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) } }; - static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects']; + static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects', 'summon']; _getTabs(tabs) { for (const v of Object.values(tabs)) { @@ -112,9 +114,24 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) return tabs; } + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + htmlElement.querySelectorAll('.summon-count-wrapper input').forEach(element => { + element.addEventListener('change', this.updateSummonCount.bind(this)); + }); + } + async _prepareContext(_options) { const context = await super._prepareContext(_options, 'action'); context.source = this.action.toObject(true); + + context.summons = []; + for (const summon of context.source.summon ?? []) { + const actor = await foundry.utils.fromUuid(summon.actorUUID); + context.summons.push({ actor, count: summon.count }); + } + context.openSection = this.openSection; context.tabs = this._getTabs(this.constructor.TABS); context.config = CONFIG.DH; @@ -207,8 +224,9 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) } static async updateForm(event, _, formData) { - const submitData = this._prepareSubmitData(event, formData), - data = foundry.utils.mergeObject(this.action.toObject(), submitData); + const submitData = this._prepareSubmitData(event, formData); + + const data = foundry.utils.mergeObject(this.action.toObject(), submitData); this.action = await this.action.update(data); this.sheetUpdate?.(this.action); @@ -227,12 +245,26 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) static removeElement(event, button) { event.stopPropagation(); const data = this.action.toObject(), - key = event.target.closest('[data-key]').dataset.key, - index = button.dataset.index; + key = event.target.closest('[data-key]').dataset.key; + + // Prefer explicit index, otherwise find by uuid + let index = button?.dataset.index; + if (index === undefined || index === null || index === '') { + const uuid = button?.dataset.uuid ?? button?.dataset.itemUuid; + index = data[key].findIndex(e => (e?.actorUUID ?? e?.uuid) === uuid); + if (index === -1) return; + } else index = Number(index); + data[key].splice(index, 1); this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); } + static async editDoc(_event, target) { + const element = target.closest('[data-item-uuid]'); + const doc = (await foundry.utils.fromUuid(element.dataset.itemUuid)) ?? null; + if (doc) return doc.sheet.render({ force: true }); + } + static addDamage(_event) { if (!this.action.damage.parts) return; const data = this.action.toObject(), @@ -304,6 +336,15 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) } } + updateSummonCount(event) { + event.stopPropagation(); + const wrapper = event.target.closest('.summon-count-wrapper'); + const index = wrapper.dataset.index; + const data = this.action.toObject(); + data.summon[index].count = event.target.value; + this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); + } + /** Specific implementation in extending classes **/ static async addEffect(_event) {} static removeEffect(_event, _button) {} @@ -313,4 +354,29 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) this.tabGroups.primary = 'base'; await super.close(options); } + + async _onDrop(event) { + const data = foundry.applications.ux.TextEditor.getDragEventData(event); + const item = await foundry.utils.fromUuid(data.uuid); + if (!(item instanceof game.system.api.documents.DhpActor)) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.summon.invalidDrop')); + return; + } + + const actionData = this.action.toObject(); + let countvalue = 1; + for (const entry of actionData.summon) { + if (entry.actorUUID === data.uuid) { + entry.count += 1; + countvalue = entry.count; + await this.constructor.updateForm.bind(this)(null, null, { + object: foundry.utils.flattenObject(actionData) + }); + return; + } + } + + actionData.summon.push({ actorUUID: data.uuid, count: countvalue }); + await this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) }); + } } diff --git a/module/applications/sheets/actors/adversary.mjs b/module/applications/sheets/actors/adversary.mjs index 98282d9f..d8a3df29 100644 --- a/module/applications/sheets/actors/adversary.mjs +++ b/module/applications/sheets/actors/adversary.mjs @@ -31,7 +31,7 @@ export default class AdversarySheet extends DHBaseActorSheet { dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]', dropSelector: null } - ], + ] }; static PARTS = { @@ -185,7 +185,6 @@ export default class AdversarySheet extends DHBaseActorSheet { super._onDragStart(event); } - /* -------------------------------------------- */ /* Application Clicks Actions */ /* -------------------------------------------- */ diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index cc42df2f..20dfea8d 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -135,7 +135,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo async actionUseButton(event, message) { const { moveIndex, actionIndex, movePath } = event.currentTarget.dataset; const targetUuid = event.currentTarget.closest('.action-use-button-parent').querySelector('select')?.value; - const parent = await foundry.utils.fromUuid(targetUuid || message.system.actor) + const parent = await foundry.utils.fromUuid(targetUuid || message.system.actor); const actionType = message.system.moves[moveIndex].actions[actionIndex]; const cls = game.system.api.models.actions.actionsTypes[actionType.type]; diff --git a/module/canvas/_module.mjs b/module/canvas/_module.mjs index 6b8885f4..c211b549 100644 --- a/module/canvas/_module.mjs +++ b/module/canvas/_module.mjs @@ -1 +1,2 @@ export * as placeables from './placeables/_module.mjs'; +export { default as DhTokenLayer } from './tokens.mjs'; diff --git a/module/canvas/placeables/token.mjs b/module/canvas/placeables/token.mjs index e8b85938..d8acb73a 100644 --- a/module/canvas/placeables/token.mjs +++ b/module/canvas/placeables/token.mjs @@ -1,4 +1,12 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { + /** @inheritdoc */ + async _draw(options) { + await super._draw(options); + + if (this.document.flags.daggerheart?.createPlacement) + this.previewHelp ||= this.addChild(this.#drawPreviewHelp()); + } + /** @inheritDoc */ async _drawEffects() { this.effects.renderable = false; @@ -34,7 +42,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { this.renderFlags.set({ refreshEffects: true }); } - /** + /** * Returns the distance from this token to another token object. * This value is corrected to handle alternate token sizes and other grid types * according to the diagonal rules. @@ -47,11 +55,11 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { const destinationPoint = target.center; // Compute for gridless. This version returns circular edge to edge + grid distance, - // so that tokens that are touching return 5. + // so that tokens that are touching return 5. if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) { const boundsCorrection = canvas.grid.distance / canvas.grid.size; - const originRadius = this.bounds.width * boundsCorrection / 2; - const targetRadius = target.bounds.width * boundsCorrection / 2; + const originRadius = (this.bounds.width * boundsCorrection) / 2; + const targetRadius = (target.bounds.width * boundsCorrection) / 2; const distance = canvas.grid.measurePath([originPoint, destinationPoint]).distance; return distance - originRadius - targetRadius + canvas.grid.distance; } @@ -61,11 +69,11 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { const targetEdge = this.#getEdgeBoundary(target.bounds, originPoint, destinationPoint); const adjustedOriginPoint = canvas.grid.getTopLeftPoint({ x: originEdge.x + Math.sign(originPoint.x - originEdge.x), - y: originEdge.y + Math.sign(originPoint.y - originEdge.y) + y: originEdge.y + Math.sign(originPoint.y - originEdge.y) }); const adjustDestinationPoint = canvas.grid.getTopLeftPoint({ x: targetEdge.x + Math.sign(destinationPoint.x - targetEdge.x), - y: targetEdge.y + Math.sign(destinationPoint.y - targetEdge.y) + y: targetEdge.y + Math.sign(destinationPoint.y - targetEdge.y) }); return canvas.grid.measurePath([adjustedOriginPoint, adjustDestinationPoint]).distance; } @@ -94,7 +102,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { /** Tests if the token is at least adjacent with another, with some leeway for diagonals */ isAdjacentWith(token) { - return this.distanceTo(token) <= (canvas.grid.distance * 1.5); + return this.distanceTo(token) <= canvas.grid.distance * 1.5; } /** @inheritDoc */ @@ -132,4 +140,25 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { bar.position.set(0, posY); return true; } + + /** + * Draw a helptext for previews as a text object + * @returns {PreciseText} The Text object for the preview helper + */ + #drawPreviewHelp() { + const { uiScale } = canvas.dimensions; + + const textStyle = CONFIG.canvasTextStyle.clone(); + textStyle.fontSize = 18; + textStyle.wordWrapWidth = this.w * 2.5; + textStyle.fontStyle = 'italic'; + + const helpText = new PreciseText( + `(${game.i18n.localize('DAGGERHEART.UI.Tooltip.previewTokenHelp')})`, + textStyle + ); + helpText.anchor.set(helpText.width / 900, 1); + helpText.scale.set(uiScale, uiScale); + return helpText; + } } diff --git a/module/canvas/tokens.mjs b/module/canvas/tokens.mjs new file mode 100644 index 00000000..9813cd48 --- /dev/null +++ b/module/canvas/tokens.mjs @@ -0,0 +1,16 @@ +export default class DhTokenLayer extends foundry.canvas.layers.TokenLayer { + async _createPreview(createData, options) { + if (options.actor) { + const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes; + if (options.actor?.system.metadata.usesSize) { + const tokenSize = tokenSizes[options.actor.system.size]; + if (tokenSize && options.actor.system.size !== CONFIG.DH.ACTOR.tokenSize.custom.id) { + createData.width = tokenSize; + createData.height = tokenSize; + } + } + } + + return super._createPreview(createData, options); + } +} diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index 9f9499ac..dac4cf68 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -166,7 +166,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel */ getRollData(data = {}) { const actorData = this.actor ? this.actor.getRollData(false) : {}; - actorData.result = data.roll?.total ?? 1; actorData.scale = data.costs?.length // Right now only return the first scalable cost. ? (data.costs.find(c => c.scalable)?.total ?? 1) diff --git a/module/data/action/summonAction.mjs b/module/data/action/summonAction.mjs index b06f1d38..1505ce2d 100644 --- a/module/data/action/summonAction.mjs +++ b/module/data/action/summonAction.mjs @@ -1,19 +1,5 @@ import DHBaseAction from './baseAction.mjs'; export default class DHSummonAction extends DHBaseAction { - static defineSchema() { - const fields = foundry.data.fields; - return { - ...super.defineSchema(), - documentUUID: new fields.DocumentUUIDField({ type: 'Actor' }) - }; - } - - async trigger(event, ...args) { - if (!this.canSummon || !canvas.scene) return; - } - - get canSummon() { - return game.user.can('TOKEN_CREATE'); - } + static extraSchemas = [...super.extraSchemas, 'summon']; } diff --git a/module/data/fields/action/_module.mjs b/module/data/fields/action/_module.mjs index ef69394a..0bdffca2 100644 --- a/module/data/fields/action/_module.mjs +++ b/module/data/fields/action/_module.mjs @@ -9,3 +9,4 @@ export { default as BeastformField } from './beastformField.mjs'; export { default as DamageField } from './damageField.mjs'; export { default as RollField } from './rollField.mjs'; export { default as MacroField } from './macroField.mjs'; +export { default as SummonField } from './summonField.mjs'; diff --git a/module/data/fields/action/summonField.mjs b/module/data/fields/action/summonField.mjs new file mode 100644 index 00000000..dce6414c --- /dev/null +++ b/module/data/fields/action/summonField.mjs @@ -0,0 +1,89 @@ +import FormulaField from '../formulaField.mjs'; + +const fields = foundry.data.fields; + +export default class DHSummonField extends fields.ArrayField { + /** + * Action Workflow order + */ + static order = 120; + + constructor(options = {}, context = {}) { + const summonFields = new fields.SchemaField({ + actorUUID: new fields.DocumentUUIDField({ + type: 'Actor', + required: true + }), + count: new FormulaField({ + required: true, + default: '1' + }) + }); + super(summonFields, options, context); + } + + static async execute() { + if (!canvas.scene) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.summon.error')); + return; + } + + if (this.summon.length === 0) { + ui.notifications.warn('No actors configured for this Summon action.'); + return; + } + + const rolls = []; + const summonData = []; + for (const summon of this.summon) { + let count = summon.count; + const roll = new Roll(summon.count); + if (!roll.isDeterministic) { + await roll.evaluate(); + if (game.modules.get('dice-so-nice')?.active) rolls.push(roll); + count = roll.total; + } + + const actor = DHSummonField.getWorldActor(await foundry.utils.fromUuid(summon.actorUUID)); + /* Extending summon data in memory so it's available in actionField.toChat. Think it's harmless, but ugly. Could maybe find a better way. */ + summon.rolledCount = count; + summon.actor = actor.toObject(); + + summonData.push({ actor, count: count }); + } + + if (rolls.length) await Promise.all(rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true))); + + this.actor.sheet?.minimize(); + DHSummonField.handleSummon(summonData, this.actor); + } + + /* Check for any available instances of the actor present in the world if we're missing artwork in the compendium */ + static getWorldActor(baseActor) { + const dataType = game.system.api.data.actors[`Dh${baseActor.type.capitalize()}`]; + if (baseActor.inCompendium && dataType && baseActor.img === dataType.DEFAULT_ICON) { + const worldActorCopy = game.actors.find(x => x.name === baseActor.name); + return worldActorCopy ?? baseActor; + } + + return baseActor; + } + + static async handleSummon(summonData, actionActor, summonIndex = 0) { + const summon = summonData[summonIndex]; + const result = await CONFIG.ux.TokenManager.createPreviewAsync(summon.actor, { + name: `${summon.actor.prototypeToken.name}${summon.count > 1 ? ` (${summon.count}x)` : ''}` + }); + + if (!result) return actionActor.sheet?.maximize(); + summon.actor = result.actor; + + summon.count--; + if (summon.count <= 0) { + summonIndex++; + if (summonIndex === summonData.length) return actionActor.sheet?.maximize(); + } + + DHSummonField.handleSummon(summonData, actionActor, summonIndex); + } +} diff --git a/module/data/fields/actionField.mjs b/module/data/fields/actionField.mjs index d0d04721..0d71ab86 100644 --- a/module/data/fields/actionField.mjs +++ b/module/data/fields/actionField.mjs @@ -267,7 +267,8 @@ export function ActionMixin(Base) { action: { name: this.name, img: this.baseAction ? this.parent.parent.img : this.img, - tags: this.tags ? this.tags : ['Spell', 'Arcana', 'Lv 10'] + tags: this.tags ? this.tags : ['Spell', 'Arcana', 'Lv 10'], + summon: this.summon }, itemOrigin: this.item, description: this.description || (this.item instanceof Item ? this.item.system.description : '') diff --git a/module/documents/_module.mjs b/module/documents/_module.mjs index 22718bea..8073cfe1 100644 --- a/module/documents/_module.mjs +++ b/module/documents/_module.mjs @@ -8,3 +8,4 @@ export { default as DhScene } from './scene.mjs'; export { default as DhToken } from './token.mjs'; export { default as DhTooltipManager } from './tooltipManager.mjs'; export { default as DhTemplateManager } from './templateManager.mjs'; +export { default as DhTokenManager } from './tokenManager.mjs'; diff --git a/module/documents/tokenManager.mjs b/module/documents/tokenManager.mjs new file mode 100644 index 00000000..be5467da --- /dev/null +++ b/module/documents/tokenManager.mjs @@ -0,0 +1,104 @@ +/** + * A singleton class that handles preview tokens. + */ + +export default class DhTokenManager { + #activePreview; + #actor; + #resolve; + + /** + * Create a template preview, deactivating any existing ones. + * @param {object} data + */ + async createPreview(actor, tokenData) { + this.#actor = actor; + const token = await canvas.tokens._createPreview( + { + ...actor.prototypeToken, + displayName: 50, + ...tokenData + }, + { renderSheet: false, actor } + ); + + this.#activePreview = { + document: token.document, + object: token, + origin: { x: token.document.x, y: token.document.y } + }; + + this.#activePreview.events = { + contextmenu: this.#cancelTemplate.bind(this), + mousedown: this.#confirmTemplate.bind(this), + mousemove: this.#onDragMouseMove.bind(this) + }; + + canvas.stage.on('mousemove', this.#activePreview.events.mousemove); + canvas.stage.on('mousedown', this.#activePreview.events.mousedown); + canvas.app.view.addEventListener('contextmenu', this.#activePreview.events.contextmenu); + } + + /* Currently intended for using as a preview of where to create a token. (note the flag) */ + async createPreviewAsync(actor, tokenData = {}) { + return new Promise(resolve => { + this.#resolve = resolve; + this.createPreview(actor, { ...tokenData, flags: { daggerheart: { createPlacement: true } } }); + }); + } + + /** + * Handles the movement of the token preview on mousedrag. + * @param {mousemove Event} event + */ + #onDragMouseMove(event) { + event.stopPropagation(); + const { moveTime, object } = this.#activePreview; + const update = {}; + + const now = Date.now(); + if (now - (moveTime || 0) <= 16) return; + this.#activePreview.moveTime = now; + + let cursor = event.getLocalPosition(canvas.templates); + + Object.assign(update, canvas.grid.getTopLeftPoint(cursor)); + + object.document.updateSource(update); + object.renderFlags.set({ refresh: true }); + } + + /** + * Cancels the preview token on right-click. + * @param {contextmenu Event} event + */ + #cancelTemplate(_event, resolved) { + const { mousemove, mousedown, contextmenu } = this.#activePreview.events; + this.#activePreview.object.destroy(); + + canvas.stage.off('mousemove', mousemove); + canvas.stage.off('mousedown', mousedown); + canvas.app.view.removeEventListener('contextmenu', contextmenu); + if (this.#resolve && !resolved) this.#resolve(false); + } + + /** + * Creates a real Actor and token at the preview location and cancels the preview. + * @param {click Event} event + */ + async #confirmTemplate(event) { + event.stopPropagation(); + this.#cancelTemplate(event, true); + + const actor = this.#actor.inCompendium + ? await game.system.api.documents.DhpActor.create(this.#actor.toObject()) + : this.#actor; + const tokenData = await actor.getTokenDocument(); + const result = await canvas.scene.createEmbeddedDocuments('Token', [ + { ...tokenData, x: this.#activePreview.document.x, y: this.#activePreview.document.y } + ]); + + this.#activePreview = undefined; + if (this.#resolve && result.length) this.#resolve(result[0]); + } +} diff --git a/module/systemRegistration/handlebars.mjs b/module/systemRegistration/handlebars.mjs index 32e047fd..97769181 100644 --- a/module/systemRegistration/handlebars.mjs +++ b/module/systemRegistration/handlebars.mjs @@ -32,6 +32,7 @@ export const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/actionTypes/effect.hbs', 'systems/daggerheart/templates/actionTypes/beastform.hbs', 'systems/daggerheart/templates/actionTypes/countdown.hbs', + 'systems/daggerheart/templates/actionTypes/summon.hbs', 'systems/daggerheart/templates/settings/components/settings-item-line.hbs', 'systems/daggerheart/templates/ui/tooltip/parts/tooltipChips.hbs', 'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs', diff --git a/src/packs/adversaries/adversary_Arch_Necromancer_WPEOIGfclNJxWb87.json b/src/packs/adversaries/adversary_Arch_Necromancer_WPEOIGfclNJxWb87.json index 4fc58990..d4e506cb 100644 --- a/src/packs/adversaries/adversary_Arch_Necromancer_WPEOIGfclNJxWb87.json +++ b/src/packs/adversaries/adversary_Arch_Necromancer_WPEOIGfclNJxWb87.json @@ -533,33 +533,31 @@ "description": "

Spend a Fear to summon a @UUID[Compendium.daggerheart.adversaries.Actor.YhJrP7rTBiRdX5Fp]{Zombie Legion}, which appears at Close range and immediately takes the spotlight.

", "resource": null, "actions": { - "gZg3AkzCYUTExjE6": { - "type": "effect", - "_id": "gZg3AkzCYUTExjE6", + "qSuWxC8xQOhnbBx9": { + "type": "summon", + "_id": "qSuWxC8xQOhnbBx9", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "fear", - "value": 1, - "step": null - } - ], + "cost": [], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.YhJrP7rTBiRdX5Fp", + "count": "1" + } + ], "name": "Spend Fear", - "img": "icons/magic/death/undead-zombie-grave-green.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json b/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json index 9e838d6d..800e7305 100644 --- a/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json +++ b/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json @@ -457,11 +457,12 @@ "img": "icons/creatures/unholy/demon-fire-horned-clawed.webp", "range": "" }, - "7G6uWlFEeOLsJIWY": { - "type": "effect", - "_id": "7G6uWlFEeOLsJIWY", + "FlE6i0tbKEguF9wz": { + "type": "summon", + "_id": "FlE6i0tbKEguF9wz", "systemPath": "actions", - "description": "

Summon [[/r 1d4]]@UUID[Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb]{Minor Demons}, who appear at Close range.

", + "baseAction": false, + "description": "", "chatDisplay": true, "originItem": { "type": "itemCollection" @@ -474,13 +475,13 @@ "recovery": null, "consumeOnSuccess": false }, - "effects": [], - "target": { - "type": "any", - "amount": null - }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb", + "count": "1d4" + } + ], "name": "Summon", - "img": "icons/creatures/unholy/demon-fire-horned-clawed.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Dryad_wR7cFKrHvRzbzhBT.json b/src/packs/adversaries/adversary_Dryad_wR7cFKrHvRzbzhBT.json index f0a5d81c..ca9ce647 100644 --- a/src/packs/adversaries/adversary_Dryad_wR7cFKrHvRzbzhBT.json +++ b/src/packs/adversaries/adversary_Dryad_wR7cFKrHvRzbzhBT.json @@ -363,33 +363,31 @@ "description": "

Spend a Fear to grow three @UUID[Compendium.daggerheart.adversaries.Actor.o63nS0k3wHu6EgKP]{Treant Sapling Minions}, who appear at Close range and immediately take the spotlight.

", "resource": null, "actions": { - "84Q2b0zIY9c7Yhho": { - "type": "effect", - "_id": "84Q2b0zIY9c7Yhho", + "R84DdS0OIx2cUt1w": { + "type": "summon", + "_id": "R84DdS0OIx2cUt1w", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "fear", - "value": 1, - "step": null - } - ], + "cost": [], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "self", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.o63nS0k3wHu6EgKP", + "count": "3" + } + ], "name": "Spend Fear", - "img": "icons/magic/unholy/orb-hands-pink.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Green_Ooze_SHXedd9zZPVfUgUa.json b/src/packs/adversaries/adversary_Green_Ooze_SHXedd9zZPVfUgUa.json index c7446a11..b03b5495 100644 --- a/src/packs/adversaries/adversary_Green_Ooze_SHXedd9zZPVfUgUa.json +++ b/src/packs/adversaries/adversary_Green_Ooze_SHXedd9zZPVfUgUa.json @@ -510,34 +510,41 @@ "description": "

When the @Lookup[@name] has 3 or more HP marked, you can spend a Fear to split them into two @UUID[Compendium.daggerheart.adversaries.Actor.aLkLFuVoKz2NLoBK]{Tiny Green Oozes} (with no marked HP or Stress). Immediately spotlight both of them.

", "resource": null, "actions": { - "s5mLw6DRGd76MLcC": { - "type": "effect", - "_id": "s5mLw6DRGd76MLcC", + "J8U7dw3cDSsEirr5": { + "type": "summon", + "_id": "J8U7dw3cDSsEirr5", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "fear", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "self", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.aLkLFuVoKz2NLoBK", + "count": "2" + } + ], "name": "Spend Fear", - "img": "icons/creatures/slimes/slime-movement-pseudopods-green.webp", - "range": "" + "range": "self" } }, "originItemType": null, diff --git a/src/packs/adversaries/adversary_Head_Vampire_i2UNbRvgyoSs07M6.json b/src/packs/adversaries/adversary_Head_Vampire_i2UNbRvgyoSs07M6.json index 9e948594..d5891359 100644 --- a/src/packs/adversaries/adversary_Head_Vampire_i2UNbRvgyoSs07M6.json +++ b/src/packs/adversaries/adversary_Head_Vampire_i2UNbRvgyoSs07M6.json @@ -474,33 +474,31 @@ "description": "

Spend 2 Fear to summon [[/r 1d4]] @UUID[Compendium.daggerheart.adversaries.Actor.WWyUp6Mxl1S3KYUG]{Vampires}, who appear at Far range and immediately take the spotlight.

", "resource": null, "actions": { - "5Q6RMUTiauKw0tDj": { - "type": "effect", - "_id": "5Q6RMUTiauKw0tDj", + "jGFOnU6PNdWU6iF4": { + "type": "summon", + "_id": "jGFOnU6PNdWU6iF4", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "fear", - "value": 2, - "step": null - } - ], + "cost": [], "uses": { "value": null, "max": "", - "recovery": null + "recovery": null, + "consumeOnSuccess": false }, - "effects": [], - "target": { - "type": "any", - "amount": null - }, - "name": "Summon Vampires", - "img": "icons/creatures/mammals/bat-giant-tattered-purple.webp", + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.WWyUp6Mxl1S3KYUG", + "count": "1d4" + } + ], + "name": "Spend Fear", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json b/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json index 6f64f883..3bb8ae96 100644 --- a/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json +++ b/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json @@ -479,33 +479,31 @@ "description": "

When the @Lookup[@name] has 4 or more HP marked, you can spend a Fear to split them into two @UUID[Compendium.daggerheart.adversaries.Actor.SHXedd9zZPVfUgUa]{Green Oozes}(with no marked HP or Stress). Immediately spotlight both of them.

", "resource": null, "actions": { - "iQsYAqpUFvJslRDr": { - "type": "effect", - "_id": "iQsYAqpUFvJslRDr", + "aeRdkiRsDNagTKhp": { + "type": "summon", + "_id": "aeRdkiRsDNagTKhp", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "fear", - "value": 1, - "step": null - } - ], + "cost": [], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.SHXedd9zZPVfUgUa", + "count": "2" + } + ], "name": "Spend Fear", - "img": "icons/creatures/slimes/slime-movement-pseudopods-green.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Jagged_Knife_Lieutenant_aTljstqteGoLpCBq.json b/src/packs/adversaries/adversary_Jagged_Knife_Lieutenant_aTljstqteGoLpCBq.json index 165bb160..c139d76f 100644 --- a/src/packs/adversaries/adversary_Jagged_Knife_Lieutenant_aTljstqteGoLpCBq.json +++ b/src/packs/adversaries/adversary_Jagged_Knife_Lieutenant_aTljstqteGoLpCBq.json @@ -287,7 +287,35 @@ "system": { "description": "

Summon three @Compendium[daggerheart.adversaries.Actor.C0OMQqV7pN6t7ouR], who appear at Far range.

", "resource": null, - "actions": {}, + "actions": { + "MCTBsw9lusUdubj0": { + "type": "summon", + "_id": "MCTBsw9lusUdubj0", + "systemPath": "actions", + "baseAction": false, + "description": "", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.C0OMQqV7pN6t7ouR", + "count": "3" + } + ], + "name": "Summon", + "range": "" + } + }, "originItemType": null, "subType": null, "originId": null, diff --git a/src/packs/adversaries/adversary_Petty_Noble_wycLpvebWdUqRhpP.json b/src/packs/adversaries/adversary_Petty_Noble_wycLpvebWdUqRhpP.json index 4ac7e746..db284f40 100644 --- a/src/packs/adversaries/adversary_Petty_Noble_wycLpvebWdUqRhpP.json +++ b/src/packs/adversaries/adversary_Petty_Noble_wycLpvebWdUqRhpP.json @@ -258,57 +258,40 @@ "description": "

Once per scene, mark a Stress to summon 1d4 @UUID[Compendium.daggerheart.adversaries.Actor.B4LZcGuBAHzyVdzy]{Bladed Guards}, who appear at Far range to enforce the @Lookup[@name]’s will.

", "resource": null, "actions": { - "cUKwhq1imsTVru8D": { - "type": "attack", - "_id": "cUKwhq1imsTVru8D", + "tioTtYfIGFIXRITN": { + "type": "summon", + "_id": "tioTtYfIGFIXRITN", "systemPath": "actions", - "description": "

Once per scene, mark a Stress to summon 1d4 @UUID[Compendium.daggerheart.adversaries.Actor.B4LZcGuBAHzyVdzy]{Bladed Guards}, who appear at Far range to enforce the Noble’s will.

", + "baseAction": false, + "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "stress", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, - "max": "", - "recovery": null - }, - "damage": { - "parts": [], - "includeBase": false - }, - "target": { - "type": "any", - "amount": null - }, - "effects": [], - "roll": { - "type": "diceSet", - "trait": null, - "difficulty": null, - "bonus": null, - "advState": "neutral", - "diceRolling": { - "multiplier": "prof", - "flatMultiplier": 1, - "dice": "d4", - "compare": null, - "treshold": null - }, - "useDefault": false - }, - "save": { - "trait": null, - "difficulty": null, - "damageMod": "none" + "max": "1", + "recovery": "scene", + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.B4LZcGuBAHzyVdzy", + "count": "1d4" + } + ], "name": "Summon Guards", - "img": "icons/environment/people/infantry-armored.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Pirate_Captain_OROJbjsqagVh7ECV.json b/src/packs/adversaries/adversary_Pirate_Captain_OROJbjsqagVh7ECV.json index 409d7698..5b00ec60 100644 --- a/src/packs/adversaries/adversary_Pirate_Captain_OROJbjsqagVh7ECV.json +++ b/src/packs/adversaries/adversary_Pirate_Captain_OROJbjsqagVh7ECV.json @@ -313,36 +313,43 @@ "_id": "WGEGO0DSOs5cF0EL", "img": "icons/environment/people/charge.webp", "system": { - "description": "

Once per scene, mark a Stress to summon a Pirate Raiders Horde, which appears at Far range.

", + "description": "

Once per scene, mark a Stress to summon a @UUID[Compendium.daggerheart.adversaries.Actor.5YgEajn0wa4i85kC]{Pirate Raider Horde}, which appears at Far range.

", "resource": null, "actions": { - "NlgIp0KrmZoS27Xy": { - "type": "effect", - "_id": "NlgIp0KrmZoS27Xy", + "nuYk5WeLLpIKa69q": { + "type": "summon", + "_id": "nuYk5WeLLpIKa69q", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "stress", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.5YgEajn0wa4i85kC", + "count": "1" + } + ], "name": "Mark Stress", - "img": "icons/environment/people/charge.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Red_Ooze_9rVlbJVrDNn1x7PS.json b/src/packs/adversaries/adversary_Red_Ooze_9rVlbJVrDNn1x7PS.json index 320b71af..2c10ae3f 100644 --- a/src/packs/adversaries/adversary_Red_Ooze_9rVlbJVrDNn1x7PS.json +++ b/src/packs/adversaries/adversary_Red_Ooze_9rVlbJVrDNn1x7PS.json @@ -454,33 +454,40 @@ "description": "

When the @Lookup[@name] has 3 or more HP marked, you can spend a Fear to split them into two @UUID[Compendium.daggerheart.adversaries.Actor.1fkLQXVtmILqfJ44]{Tiny Red Oozes} (with no marked HP or Stress). Immediately spotlight both of them.

", "resource": null, "actions": { - "dw6Juw8mriH7sg0e": { - "type": "effect", - "_id": "dw6Juw8mriH7sg0e", + "BMEr77hDxaQyYBna": { + "type": "summon", + "_id": "BMEr77hDxaQyYBna", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "fear", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.1fkLQXVtmILqfJ44", + "count": "2" + } + ], "name": "Spend Fear", - "img": "icons/creatures/slimes/slime-movement-splashing-red.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Secret_Keeper_sLAccjvCWfeedbpI.json b/src/packs/adversaries/adversary_Secret_Keeper_sLAccjvCWfeedbpI.json index 0c8757c5..d17c3f86 100644 --- a/src/packs/adversaries/adversary_Secret_Keeper_sLAccjvCWfeedbpI.json +++ b/src/packs/adversaries/adversary_Secret_Keeper_sLAccjvCWfeedbpI.json @@ -416,28 +416,6 @@ "description": "

Countdown (6). When the @Lookup[@name] is in the spotlight for the first time, activate the countdown. When they mark HP, tick down this countdown by the number of HP marked. When it triggers, summon a @UUID[Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb]{Minor Demon} who appears at Close range.

", "resource": null, "actions": { - "0rixG6jLRynAYNqA": { - "type": "effect", - "_id": "0rixG6jLRynAYNqA", - "systemPath": "actions", - "description": "

Summon a @UUID[Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb]{Minor Demon} who appears at Close range.

", - "chatDisplay": true, - "actionType": "action", - "cost": [], - "uses": { - "value": null, - "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null - }, - "name": "Summon", - "img": "icons/magic/unholy/silhouette-light-fire-blue.webp", - "range": "close" - }, "ZVXHY2fpomoKV7jG": { "type": "countdown", "_id": "ZVXHY2fpomoKV7jG", @@ -474,6 +452,33 @@ "name": "Start Countdown", "img": "icons/magic/unholy/silhouette-light-fire-blue.webp", "range": "" + }, + "YReYG6DrWp4QGSij": { + "type": "summon", + "_id": "YReYG6DrWp4QGSij", + "systemPath": "actions", + "baseAction": false, + "description": "", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb", + "count": "1" + } + ], + "name": "Summon", + "range": "" } }, "originItemType": null, @@ -502,33 +507,31 @@ "description": "

Once per scene, when the @Lookup[@name] marks 2 or more HP, you can mark a Stress to summon a @UUID[Compendium.daggerheart.adversaries.Actor.NoRZ1PqB8N5wcIw0]{Demonic Hound Pack}, which appears at Close range and is immediately spotlighted.

", "resource": null, "actions": { - "JBuQUJhif2A7IlJd": { - "type": "effect", - "_id": "JBuQUJhif2A7IlJd", + "tfmY6HYkkY27NBaF": { + "type": "summon", + "_id": "tfmY6HYkkY27NBaF", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "stress", - "value": 1, - "step": null - } - ], + "cost": [], "uses": { "value": null, - "max": "1", - "recovery": "scene" - }, - "effects": [], - "target": { - "type": "self", - "amount": null + "max": "", + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.NoRZ1PqB8N5wcIw0", + "count": "1" + } + ], "name": "Mark Stress", - "img": "icons/creatures/unholy/demon-fire-horned-clawed.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json b/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json index a6e5ca17..0f1ba28f 100644 --- a/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json +++ b/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json @@ -340,7 +340,35 @@ "system": { "description": "

When an attack from the @Lookup[@name] causes a target to mark HP and there are three or more @Lookup[@name] Minions within Close range, you can combine the Minions into a @UUID[Compendium.daggerheart.adversaries.Actor.PKSXFuaIHUCoH63A]{Tangle Bramble Swarm Horde}. The Horde’s HP is equal to the number of Minions combined.

", "resource": null, - "actions": {}, + "actions": { + "g1OQ5xlMHFWsoktd": { + "type": "summon", + "_id": "g1OQ5xlMHFWsoktd", + "systemPath": "actions", + "baseAction": false, + "description": "", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.PKSXFuaIHUCoH63A", + "count": "1" + } + ], + "name": "Summon", + "range": "" + } + }, "originItemType": null, "subType": null, "originId": null, diff --git a/src/packs/environments/environment_Burning_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json b/src/packs/environments/environment_Burning_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json index dc42fb07..ea4f1951 100644 --- a/src/packs/environments/environment_Burning_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json +++ b/src/packs/environments/environment_Burning_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json @@ -314,7 +314,7 @@ "name": "Charcoal Constructs", "type": "feature", "system": { - "description": "

Warped animals wreathed in indigo f l ame trample through a point of your choice. All targets within Close range of that point must make an Agility Reaction Roll. Targets who fail take 3d12+3 physical damage. Targets who succeed take half damage instead.

@Template[type:emanation|range:c]

Are these real animals consumed by the fl ame or merely constructs of the corrupting magic?

", + "description": "

Warped animals wreathed in indigo flame trample through a point of your choice. All targets within Close range of that point must make an Agility Reaction Roll. Targets who fail take 3d12+3 physical damage. Targets who succeed take half damage instead.

@Template[type:emanation|range:c]

Are these real animals consumed by the fl ame or merely constructs of the corrupting magic?

", "resource": null, "actions": { "gbXIaKr8em134IZC": { diff --git a/src/packs/environments/environment_Chaos_Realm_2Z1mKc65LxNk2PqR.json b/src/packs/environments/environment_Chaos_Realm_2Z1mKc65LxNk2PqR.json index 77781de0..361b15bc 100644 --- a/src/packs/environments/environment_Chaos_Realm_2Z1mKc65LxNk2PqR.json +++ b/src/packs/environments/environment_Chaos_Realm_2Z1mKc65LxNk2PqR.json @@ -467,33 +467,48 @@ "description": "

Spend a Fear to summon an @UUID[Compendium.daggerheart.adversaries.Actor.A0SeeDzwjvqOsyof]{Outer Realms Abomination}, an@UUID[Compendium.daggerheart.adversaries.Actor.ms6nuOl3NFkhPj1k]{Outer Realms Corrupter}, and [[/r 2d6]] @UUID[Compendium.daggerheart.adversaries.Actor.moJhHgKqTKPS2WYS]{Outer Realms Thrall}, who appear at Close range of a chosen PC in defiance of logic and causality. Immediately spotlight one of these adversaries, and you can spend an additional Fear to automatically succeed on that adversary’s standard attack.

What halfconsumed remnants of the shattered world do these monstrosities cast aside in pursuit of living flesh? What jagged refl ections of former personhood do you catch between moments of unquestioning malice?

", "resource": null, "actions": { - "5a8ESNroEQHAm7rO": { - "type": "effect", - "_id": "5a8ESNroEQHAm7rO", + "KCzdCu2KhAx9KyhT": { + "type": "summon", + "_id": "KCzdCu2KhAx9KyhT", "systemPath": "actions", - "description": "

Spend a Fear to summon an @UUID[Compendium.daggerheart.adversaries.Actor.A0SeeDzwjvqOsyof]{Outer Realms Abomination}, an@UUID[Compendium.daggerheart.adversaries.Actor.ms6nuOl3NFkhPj1k]{Outer Realms Corrupter}, and [[/r 2d6]] @UUID[Compendium.daggerheart.adversaries.Actor.moJhHgKqTKPS2WYS]{Outer Realms Thrall}, who appear at Close range of a chosen PC in defiance of logic and causality. Immediately spotlight one of these adversaries, and you can spend an additional Fear to automatically succeed on that adversary’s standard attack.

What halfconsumed remnants of the shattered world do these monstrosities cast aside in pursuit of living flesh? What jagged refl ections of former personhood do you catch between moments of unquestioning malice?

", + "baseAction": false, + "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "fear", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.A0SeeDzwjvqOsyof", + "count": "1" + }, + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.ms6nuOl3NFkhPj1k", + "count": "1" + }, + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.moJhHgKqTKPS2WYS", + "count": "2d6" + } + ], "name": "Spend Fear", - "img": "icons/creatures/unholy/demons-horned-glowing-pink.webp", "range": "" } }, diff --git a/src/packs/environments/environment_Cult_Ritual_QAXXiOKBDmCTauHD.json b/src/packs/environments/environment_Cult_Ritual_QAXXiOKBDmCTauHD.json index 705c9585..66931f0a 100644 --- a/src/packs/environments/environment_Cult_Ritual_QAXXiOKBDmCTauHD.json +++ b/src/packs/environments/environment_Cult_Ritual_QAXXiOKBDmCTauHD.json @@ -343,11 +343,12 @@ "img": "icons/magic/unholy/barrier-fire-pink.webp", "range": "" }, - "suFEnfpOfeVRvnJF": { - "type": "effect", - "_id": "suFEnfpOfeVRvnJF", + "HG7tbEdlYl3yLQnR": { + "type": "summon", + "_id": "HG7tbEdlYl3yLQnR", "systemPath": "actions", - "description": "

Summon a @UUID[Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb]{Minor Demon} within Very Close range of the ritual’s leader.

", + "baseAction": false, + "description": "", "chatDisplay": true, "originItem": { "type": "itemCollection" @@ -360,13 +361,13 @@ "recovery": null, "consumeOnSuccess": false }, - "effects": [], - "target": { - "type": "any", - "amount": null - }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb", + "count": "1" + } + ], "name": "Summon Demon", - "img": "icons/magic/unholy/barrier-fire-pink.webp", "range": "" } }, diff --git a/src/packs/environments/environment_Divine_Usurpation_4DLYez7VbMCFDAuZ.json b/src/packs/environments/environment_Divine_Usurpation_4DLYez7VbMCFDAuZ.json index aacf87e9..d8e9cded 100644 --- a/src/packs/environments/environment_Divine_Usurpation_4DLYez7VbMCFDAuZ.json +++ b/src/packs/environments/environment_Divine_Usurpation_4DLYez7VbMCFDAuZ.json @@ -248,33 +248,31 @@ "description": "

Spend 2 Fear to summon [[/r 1d4+2]] @UUID[Compendium.daggerheart.adversaries.Actor.OsLG2BjaEdTZUJU9]{Fallen Shock Troop} that appear within Close range of the Usurper to assist their divine siege. Immediately spotlight the Shock Troops to use a “Group Attack” action.

Which High Fallen do these troops serve? Which god’s fl esh do they wish to feast upon?

", "resource": null, "actions": { - "qIQTEO5t72xFtKYI": { - "type": "effect", - "_id": "qIQTEO5t72xFtKYI", + "okcqGrI4rdghugUi": { + "type": "summon", + "_id": "okcqGrI4rdghugUi", "systemPath": "actions", - "description": "

Spend 2 Fear to summon [[/r 1d4+2]] @UUID[Compendium.daggerheart.adversaries.Actor.OsLG2BjaEdTZUJU9]{Fallen Shock Troop} that appear within Close range of the Usurper to assist their divine siege. Immediately spotlight the Shock Troops to use a “Group Attack” action.

Which High Fallen do these troops serve? Which god’s fl esh do they wish to feast upon?

", + "baseAction": false, + "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "fear", - "value": 2, - "step": null - } - ], + "cost": [], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "self", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.OsLG2BjaEdTZUJU9", + "count": "1d4+2" + } + ], "name": "Spend Fear", - "img": "icons/magic/unholy/orb-hands-pink.webp", "range": "" } }, diff --git a/src/packs/environments/environment_Mountain_Pass_acMu9wJrMZZzLSTJ.json b/src/packs/environments/environment_Mountain_Pass_acMu9wJrMZZzLSTJ.json index 8e7cf1c8..9ba6a918 100644 --- a/src/packs/environments/environment_Mountain_Pass_acMu9wJrMZZzLSTJ.json +++ b/src/packs/environments/environment_Mountain_Pass_acMu9wJrMZZzLSTJ.json @@ -246,7 +246,35 @@ "system": { "description": "

When the PCs enter the raptors’ hunting grounds, two @UUID[Compendium.daggerheart.adversaries.Actor.OMQ0v6PE8s1mSU0K]{Giant Eagles} appear at Very Far range of a chosen PC, identifying the PCs as likely prey.

How long has it been since the eagles last found prey? Do they have eggs in their nest or unfl edged young?

", "resource": null, - "actions": {}, + "actions": { + "88MyOC3IRcct6VLk": { + "type": "summon", + "_id": "88MyOC3IRcct6VLk", + "systemPath": "actions", + "baseAction": false, + "description": "", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.OMQ0v6PE8s1mSU0K", + "count": "2" + } + ], + "name": "Summon", + "range": "" + } + }, "originItemType": null, "originId": null, "featureForm": "reaction" diff --git a/src/packs/environments/environment_Raging_River_t4cdqTfzcqP3H1vJ.json b/src/packs/environments/environment_Raging_River_t4cdqTfzcqP3H1vJ.json index 5c973fa6..6c34c296 100644 --- a/src/packs/environments/environment_Raging_River_t4cdqTfzcqP3H1vJ.json +++ b/src/packs/environments/environment_Raging_River_t4cdqTfzcqP3H1vJ.json @@ -360,33 +360,40 @@ "description": "

Spend a Fear to summon a @UUID[Compendium.daggerheart.adversaries.Actor.8KWVLWXFhlY2kYx0]{Glass Snake} within Close range of a chosen PC. The Snake appears in or near the river and immediately takes the spotlight to use their “Spinning Serpent” action.

What treasures does the beast have in their burrow? What travelers have already fallen victim to this predator?

", "resource": null, "actions": { - "Mnp0Yzc7EPVXm8So": { - "type": "effect", - "_id": "Mnp0Yzc7EPVXm8So", + "uY9HMKE4Q5g7bRKg": { + "type": "summon", + "_id": "uY9HMKE4Q5g7bRKg", "systemPath": "actions", - "description": "

Spend a Fear to summon a @UUID[Compendium.daggerheart.adversaries.Actor.8KWVLWXFhlY2kYx0]{Glass Snake} within Close range of a chosen PC. The Snake appears in or near the river and immediately takes the spotlight to use their “Spinning Serpent” action.

What treasures does the beast have in their burrow? What travelers have already fallen victim to this predator?

", + "baseAction": false, + "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "fear", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "self", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.8KWVLWXFhlY2kYx0", + "count": "1" + } + ], "name": "Spend Fear", - "img": "icons/creatures/reptiles/snake-fangs-bite-green-yellow.webp", "range": "" } }, diff --git a/styles/less/sheets/actions/actions.less b/styles/less/sheets/actions/actions.less index 6796006c..07c99491 100644 --- a/styles/less/sheets/actions/actions.less +++ b/styles/less/sheets/actions/actions.less @@ -1,4 +1,53 @@ .application.daggerheart.dh-style.action-config { + .actor-summon-items { + width: 100%; + display: flex; + flex-direction: column; + gap: 10px; + + .actor-summon-line { + display: flex; + align-items: center; + gap: 5px; + border-radius: 3px; + + .actor-summon-name { + flex: 2; + display: flex; + align-items: center; + gap: 5px; + + img { + height: 40px; + } + } + + .actor-summon-controls { + flex: 1; + display: flex; + align-items: center; + gap: 5px; + + .controls { + display: flex; + gap: 5px; + } + } + } + + .summon-dragger { + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + height: 40px; + margin-top: 10px; + border: 1px dashed light-dark(@dark-blue-50, @beige-50); + border-radius: 3px; + color: light-dark(@dark-blue-50, @beige-50); + } + } + .trigger-data { width: 100%; display: flex; diff --git a/styles/less/sheets/actors/character/header.less b/styles/less/sheets/actors/character/header.less index 93d6c6be..593f1b73 100644 --- a/styles/less/sheets/actors/character/header.less +++ b/styles/less/sheets/actors/character/header.less @@ -148,7 +148,7 @@ padding: 0 0.375rem; } - button[data-action=viewParty] { + button[data-action='viewParty'] { margin-right: 6px; } } diff --git a/styles/less/sheets/index.less b/styles/less/sheets/index.less index 44a6aa4d..216cda33 100644 --- a/styles/less/sheets/index.less +++ b/styles/less/sheets/index.less @@ -39,3 +39,5 @@ @import './items/feature.less'; @import './items/heritage.less'; @import './items/item-sheet-shared.less'; + +@import './actions/actions.less'; diff --git a/styles/less/ui/chat/action.less b/styles/less/ui/chat/action.less index 817b0acd..8d309cfe 100644 --- a/styles/less/ui/chat/action.less +++ b/styles/less/ui/chat/action.less @@ -98,6 +98,61 @@ .description { padding: 8px; + + .summons-header { + font-size: var(--font-size-14); + text-align: center; + display: flex; + align-items: center; + justify-content: center; + + span { + width: 100%; + } + + &:before, + &:after { + content: ' '; + height: 1px; + width: 100%; + } + + &:before { + background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, light-dark(@dark-blue, @golden) 100%); + } + + &:after { + background: linear-gradient(90deg, light-dark(@dark-blue, @golden) 0%, rgba(0, 0, 0, 0) 100%); + } + } + + .summons-container { + display: flex; + flex-direction: column; + gap: 4px; + + .summon-container { + display: flex; + align-items: center; + justify-content: space-between; + + .summon-label-container { + flex: 1; + display: flex; + align-items: center; + gap: 4px; + + img { + height: 32px; + } + + label { + display: flex; + flex-wrap: wrap; + } + } + } + } } .ability-card-footer { diff --git a/styles/less/ui/chat/downtime.less b/styles/less/ui/chat/downtime.less index a99bde33..2875ea10 100644 --- a/styles/less/ui/chat/downtime.less +++ b/styles/less/ui/chat/downtime.less @@ -103,7 +103,7 @@ width: 100%; .action-use-target { - display:flex; + display: flex; align-items: center; justify-content: space-between; gap: 4px; @@ -127,7 +127,6 @@ font-weight: 600; height: 40px; } - } } } diff --git a/templates/actionTypes/summon.hbs b/templates/actionTypes/summon.hbs new file mode 100644 index 00000000..429977d9 --- /dev/null +++ b/templates/actionTypes/summon.hbs @@ -0,0 +1,50 @@ +
+ + {{localize "DAGGERHEART.ACTIONS.TYPES.summon.name"}} + + +
    + {{#each @root.summons as |summon index|}} +
  • +
    + +

    + {{summon.actor.name}} +

    +
    + +
    +
    +
    + +
    +
    + + +
    +
  • + {{/each}} +
    + {{localize "DAGGERHEART.ACTIONS.Settings.summon.dropSummonsHere"}} +
    +
+
\ No newline at end of file diff --git a/templates/sheets-settings/action-settings/configuration.hbs b/templates/sheets-settings/action-settings/configuration.hbs index 51b2a72b..5bd29e39 100644 --- a/templates/sheets-settings/action-settings/configuration.hbs +++ b/templates/sheets-settings/action-settings/configuration.hbs @@ -2,7 +2,7 @@ class="tab {{this.tabs.config.cssClass}}" data-group="primary" data-tab="config" -> +> {{> 'systems/daggerheart/templates/actionTypes/uses.hbs' fields=fields.uses.fields source=source.uses}} {{> 'systems/daggerheart/templates/actionTypes/cost.hbs' fields=fields.cost.element.fields source=source.cost costOptions=costOptions}} {{> 'systems/daggerheart/templates/actionTypes/range-target.hbs' fields=(object range=fields.range target=fields.target.fields) source=(object target=source.target range=source.range)}} diff --git a/templates/sheets-settings/action-settings/effect.hbs b/templates/sheets-settings/action-settings/effect.hbs index bf2f3aa1..e94f4328 100644 --- a/templates/sheets-settings/action-settings/effect.hbs +++ b/templates/sheets-settings/action-settings/effect.hbs @@ -9,5 +9,6 @@ {{#if fields.macro}}{{> 'systems/daggerheart/templates/actionTypes/macro.hbs' fields=fields.macro source=source.macro}}{{/if}} {{#if fields.effects}}{{> 'systems/daggerheart/templates/actionTypes/effect.hbs' fields=fields.effects.element.fields source=source.effects}}{{/if}} {{#if fields.beastform}}{{> 'systems/daggerheart/templates/actionTypes/beastform.hbs' fields=fields.beastform.fields source=source.beastform}}{{/if}} + {{#if fields.summon}}{{> 'systems/daggerheart/templates/actionTypes/summon.hbs' fields=fields.summon.element.fields source=source.summon}}{{/if}} {{#if fields.countdown}}{{> 'systems/daggerheart/templates/actionTypes/countdown.hbs' fields=fields.countdown.element.fields source=source.countdown}}{{/if}} \ No newline at end of file diff --git a/templates/ui/chat/action.hbs b/templates/ui/chat/action.hbs index 6b505164..65bb0762 100644 --- a/templates/ui/chat/action.hbs +++ b/templates/ui/chat/action.hbs @@ -8,6 +8,22 @@ -
{{{description}}}
+
+ {{{description}}} + {{#if action.summon}} +
{{localize "DAGGERHEART.GENERAL.summon.plural"}}
+
+ {{#each action.summon}} +
+
+ + +
+ # {{this.rolledCount}} +
+ {{/each}} +
+ {{/if}} +
\ No newline at end of file From 3c9ef75645ef840c2465c43dd878a0ebe0b45d6d Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:58:03 +0100 Subject: [PATCH 04/48] [Feature] ActiveEffect Default Hope/Fear dice (#1522) * Added ActiveEffect rules for default hope and fear dice * . * Raised system version --- lang/en.json | 2 + .../applications/sheets/actors/character.mjs | 1 + module/config/generalConfig.mjs | 2 + module/data/actor/character.mjs | 18 +++++ module/dice/dualityRoll.mjs | 7 +- ...ary_Demon_of_Despair_kE4dfhqmIQpNd44e.json | 11 ++- ...rsary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json | 73 ++++++++++++++--- ...ironment_Cult_Ritual_QAXXiOKBDmCTauHD.json | 80 ++++++++++++++++++- system.json | 2 +- 9 files changed, 180 insertions(+), 16 deletions(-) diff --git a/lang/en.json b/lang/en.json index 66955580..b84906f3 100755 --- a/lang/en.json +++ b/lang/en.json @@ -203,6 +203,8 @@ "unequip": "Unequip", "useItem": "Use Item" }, + "defaultHopeDice": "Default Hope Dice", + "defaultFearDice": "Default Fear Dice", "disadvantageSources": { "label": "Disadvantage Sources", "hint": "Add single words or short text as reminders and hints of what a character has disadvantage on." diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 51ce5bab..dd5f35fc 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -725,6 +725,7 @@ export default class CharacterSheet extends DHBaseActorSheet { }) }; const result = await this.document.diceRoll(config); + 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 diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 3f49f7aa..37894644 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -496,6 +496,8 @@ export const diceTypes = { d20: 'd20' }; +export const dieFaces = [4, 6, 8, 10, 12, 20]; + export const multiplierTypes = { prof: 'Proficiency', cast: 'Spellcast', diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index eba46f10..f6ab7e3a 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -280,6 +280,24 @@ export default class DhCharacter extends BaseDataActor { }) }) }), + dualityRoll: new fields.SchemaField({ + defaultHopeDice: new fields.NumberField({ + nullable: false, + required: true, + integer: true, + choices: CONFIG.DH.GENERAL.dieFaces, + initial: 12, + label: 'DAGGERHEART.ACTORS.Character.defaultHopeDice' + }), + defaultFearDice: new fields.NumberField({ + nullable: false, + required: true, + integer: true, + choices: CONFIG.DH.GENERAL.dieFaces, + initial: 12, + label: 'DAGGERHEART.ACTORS.Character.defaultFearDice' + }) + }), runeWard: new fields.BooleanField({ initial: false }), burden: new fields.SchemaField({ ignore: new fields.BooleanField() diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index d035a61c..155d6aa5 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -130,9 +130,12 @@ export default class DualityRoll extends D20Roll { this.terms = [this.terms[0], this.terms[1], this.terms[2]]; return; } - this.terms[0] = new foundry.dice.terms.Die({ faces: 12 }); + + const { defaultHopeDice, defaultFearDice } = this.data.rules.dualityRoll; + + this.terms[0] = new foundry.dice.terms.Die({ faces: defaultHopeDice }); this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' }); - this.terms[2] = new foundry.dice.terms.Die({ faces: 12 }); + this.terms[2] = new foundry.dice.terms.Die({ faces: defaultFearDice }); } applyAdvantage() { diff --git a/src/packs/adversaries/adversary_Demon_of_Despair_kE4dfhqmIQpNd44e.json b/src/packs/adversaries/adversary_Demon_of_Despair_kE4dfhqmIQpNd44e.json index b1804074..188b2687 100644 --- a/src/packs/adversaries/adversary_Demon_of_Despair_kE4dfhqmIQpNd44e.json +++ b/src/packs/adversaries/adversary_Demon_of_Despair_kE4dfhqmIQpNd44e.json @@ -312,7 +312,14 @@ "range": "melee" } }, - "changes": [], + "changes": [ + { + "key": "system.rules.dualityRoll.defaultHopeDice", + "mode": 5, + "value": "d8", + "priority": null + } + ], "disabled": false, "duration": { "startTime": null, @@ -323,7 +330,7 @@ "startRound": null, "startTurn": null }, - "description": "

All targets aff ected replace their Hope Die with a d8 until they roll a success with Hope or their next rest.

", + "description": "

All targets affected replace their Hope Die with a d8 until they roll a success with Hope or their next rest.

", "tint": "#ffffff", "statuses": [], "sort": 0, diff --git a/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json b/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json index 800e7305..2341ee8a 100644 --- a/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json +++ b/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json @@ -256,34 +256,45 @@ "description": "

Spend a Fear to boil the blood of all PCs within Far range. They use a d20 as their Fear Die until the end of the scene.

@Template[type:emanation|range:f]

", "resource": null, "actions": { - "V142qYppCGJn8OiN": { + "jKvzbQT0vp66DDOH": { "type": "effect", - "_id": "V142qYppCGJn8OiN", + "_id": "jKvzbQT0vp66DDOH", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "fear", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, "max": "", - "recovery": null + "recovery": null, + "consumeOnSuccess": false }, - "effects": [], + "effects": [ + { + "_id": "gFeHLGgeRoDdd3VG", + "onSave": false + } + ], "target": { - "type": "self", + "type": "hostile", "amount": null }, "name": "Spend Fear", - "img": "icons/skills/melee/maneuver-greatsword-yellow.webp", - "range": "" + "range": "far" } }, "originItemType": null, @@ -292,7 +303,51 @@ }, "_id": "a33PW8UkziliowlR", "img": "icons/skills/melee/maneuver-greatsword-yellow.webp", - "effects": [], + "effects": [ + { + "name": "Battle Lust", + "img": "icons/skills/melee/maneuver-greatsword-yellow.webp", + "origin": "Compendium.daggerheart.adversaries.Actor.5lphJAgzoqZI3VoG.Item.a33PW8UkziliowlR", + "transfer": false, + "_id": "gFeHLGgeRoDdd3VG", + "type": "base", + "system": { + "rangeDependence": { + "enabled": false, + "type": "withinRange", + "target": "hostile", + "range": "melee" + } + }, + "changes": [ + { + "key": "system.rules.dualityRoll.defaultFearDice", + "mode": 5, + "value": "d20", + "priority": null + } + ], + "disabled": false, + "duration": { + "startTime": null, + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null + }, + "description": "

You use a d20 as your Fear Die until the end of the scene.

", + "tint": "#ffffff", + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "_key": "!actors.items.effects!5lphJAgzoqZI3VoG.a33PW8UkziliowlR.gFeHLGgeRoDdd3VG" + } + ], "folder": null, "sort": 0, "ownership": { diff --git a/src/packs/environments/environment_Cult_Ritual_QAXXiOKBDmCTauHD.json b/src/packs/environments/environment_Cult_Ritual_QAXXiOKBDmCTauHD.json index 66931f0a..1295db59 100644 --- a/src/packs/environments/environment_Cult_Ritual_QAXXiOKBDmCTauHD.json +++ b/src/packs/environments/environment_Cult_Ritual_QAXXiOKBDmCTauHD.json @@ -136,13 +136,89 @@ "system": { "description": "

Cultists dedicated this place to the Fallen Gods, and their foul influence seeps into it. Reduce the PCs’ Hope Die to a d10 while in this environment. The desecration can be removed with a Progress Countdown (6).

How do the PCs fist notice that something is wrong about this place? What fears resurface while hope is kept at bay?

", "resource": null, - "actions": {}, + "actions": { + "7W3sWRLzjG3dKcgq": { + "type": "effect", + "_id": "7W3sWRLzjG3dKcgq", + "systemPath": "actions", + "baseAction": false, + "description": "", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "effects": [ + { + "_id": "8yNIw8Y7rfMdOqWC", + "onSave": false + } + ], + "target": { + "type": "any", + "amount": null + }, + "name": "Influence", + "range": "" + } + }, "originItemType": null, "originId": null }, "_id": "iiHjguQG2aBn9g8i", "img": "icons/magic/unholy/orb-contained-pink.webp", - "effects": [], + "effects": [ + { + "name": "Desecrated Ground", + "img": "icons/magic/unholy/orb-contained-pink.webp", + "origin": "Compendium.daggerheart.environments.Actor.QAXXiOKBDmCTauHD.Item.iiHjguQG2aBn9g8i", + "transfer": false, + "_id": "8yNIw8Y7rfMdOqWC", + "type": "base", + "system": { + "rangeDependence": { + "enabled": false, + "type": "withinRange", + "target": "hostile", + "range": "melee" + } + }, + "changes": [ + { + "key": "system.rules.dualityRoll.defaultHopeDice", + "mode": 5, + "value": "d10", + "priority": null + } + ], + "disabled": false, + "duration": { + "startTime": null, + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null + }, + "description": "

Your Hope Die is reduced to a d10 while in the Desecrated Grounds.

", + "tint": "#ffffff", + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "_key": "!actors.items.effects!QAXXiOKBDmCTauHD.iiHjguQG2aBn9g8i.8yNIw8Y7rfMdOqWC" + } + ], "folder": null, "sort": 0, "ownership": { diff --git a/system.json b/system.json index 5570bdbf..43f82f06 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.4.6", + "version": "1.5.0", "compatibility": { "minimum": "13.346", "verified": "13.351", From c958acabe6d2554b0153be788ac3d39fcbea91ad Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Tue, 13 Jan 2026 19:12:50 +0100 Subject: [PATCH 05/48] [Fix] Release Fixes (#1530) * Various fixes * . * . * . --- daggerheart.mjs | 9 +++++---- .../applications/scene/sceneConfigSettings.mjs | 15 +++++++-------- .../sheets/api/application-mixin.mjs | 1 + module/applications/ui/itemBrowser.mjs | 8 ++++++++ module/applications/ui/sceneNavigation.mjs | 2 +- module/canvas/placeables/token.mjs | 2 +- module/data/action/attackAction.mjs | 1 + module/dice/damageRoll.mjs | 6 ++++-- module/documents/tooltipManager.mjs | 16 +++++++++++++--- module/systemRegistration/socket.mjs | 4 ---- templates/ui/itemBrowser/itemContainer.hbs | 2 +- 11 files changed, 42 insertions(+), 24 deletions(-) diff --git a/daggerheart.mjs b/daggerheart.mjs index cd4dbc19..f27892e2 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -2,7 +2,6 @@ import { SYSTEM } from './module/config/system.mjs'; import * as applications from './module/applications/_module.mjs'; import * as data from './module/data/_module.mjs'; import * as models from './module/data/_module.mjs'; -import * as canvas from './module/canvas/_module.mjs'; import * as documents from './module/documents/_module.mjs'; import * as dice from './module/dice/_module.mjs'; import * as fields from './module/data/fields/_module.mjs'; @@ -17,7 +16,7 @@ import { settingsRegistration, socketRegistration } from './module/systemRegistration/_module.mjs'; -import { placeables } from './module/canvas/_module.mjs'; +import { placeables, DhTokenLayer } from './module/canvas/_module.mjs'; import './node_modules/@yaireo/tagify/dist/tagify.css'; import TemplateManager from './module/documents/templateManager.mjs'; import TokenManager from './module/documents/tokenManager.mjs'; @@ -53,7 +52,7 @@ CONFIG.ChatMessage.template = 'systems/daggerheart/templates/ui/chat/chat-messag CONFIG.Canvas.rulerClass = placeables.DhRuler; CONFIG.Canvas.layers.templates.layerClass = placeables.DhTemplateLayer; -CONFIG.Canvas.layers.tokens.layerClass = canvas.DhTokenLayer; +CONFIG.Canvas.layers.tokens.layerClass = DhTokenLayer; CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate; @@ -359,7 +358,9 @@ const updateAllRangeDependentEffects = async () => { const effectsAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).effects; if (!effectsAutomation.rangeDependent) return; - const tokens = canvas.scene.tokens; + const tokens = canvas.scene?.tokens; + if (!tokens) return; + if (game.user.character) { // The character updates their character's token. There can be only one token. const characterToken = tokens.find(x => x.actor === game.user.character); diff --git a/module/applications/scene/sceneConfigSettings.mjs b/module/applications/scene/sceneConfigSettings.mjs index 1b93aa8c..8a58db5c 100644 --- a/module/applications/scene/sceneConfigSettings.mjs +++ b/module/applications/scene/sceneConfigSettings.mjs @@ -5,10 +5,7 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S super(options); Hooks.on(socketEvent.Refresh, ({ refreshType }) => { - if (refreshType === RefreshType.Scene) { - this.daggerheartFlag = new game.system.api.data.scenes.DHScene(this.document.flags.daggerheart); - this.render(); - } + if (refreshType === RefreshType.Scene) this.render(); }); } @@ -42,7 +39,9 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S async _preRender(context, options) { await super._preFirstRender(context, options); - this.daggerheartFlag = new game.system.api.data.scenes.DHScene(this.document.flags.daggerheart); + + if (!options.internalRefresh) + this.daggerheartFlag = new game.system.api.data.scenes.DHScene(this.document.flags.daggerheart); } _attachPartListeners(partId, htmlElement, options) { @@ -52,7 +51,7 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S case 'dh': htmlElement.querySelector('#rangeMeasurementSetting')?.addEventListener('change', async event => { this.daggerheartFlag.updateSource({ rangeMeasurement: { setting: event.target.value } }); - this.render(); + this.render({ internalRefresh: true }); }); const dragArea = htmlElement.querySelector('.scene-environments'); @@ -69,7 +68,7 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S await this.daggerheartFlag.updateSource({ sceneEnvironments: [...this.daggerheartFlag.sceneEnvironments, data.uuid] }); - this.render(); + this.render({ internalRefresh: true }); } } @@ -92,7 +91,7 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S (_, index) => index !== Number.parseInt(button.dataset.index) ) }); - this.render(); + this.render({ internalRefresh: true }); } /** @override */ diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 903caa2a..7276316f 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -505,6 +505,7 @@ export default function DHApplicationMixin(Base) { const doc = await getDocFromElement(target), action = doc?.system?.attack ?? doc; const config = action.prepareConfig(event); + config.effects = Array.from(await this.document.allApplicableEffects()); config.hasRoll = false; return action && action.workflow.get('damage').execute(config, null, true); } diff --git a/module/applications/ui/itemBrowser.mjs b/module/applications/ui/itemBrowser.mjs index 794c3fb6..b35573f7 100644 --- a/module/applications/ui/itemBrowser.mjs +++ b/module/applications/ui/itemBrowser.mjs @@ -230,6 +230,14 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { result.flatMap(r => r), 'name' ); + + /* If any noticeable slowdown occurs, consider replacing with enriching description on clicking to expand descriptions */ + for (const item of this.items) { + item.system.enrichedDescription = + (await item.system.getEnrichedDescription?.()) ?? + (await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.description)); + } + this.fieldFilter = this._createFieldFilter(); if (this.presets?.filter) { diff --git a/module/applications/ui/sceneNavigation.mjs b/module/applications/ui/sceneNavigation.mjs index ac16ac99..0a3e08a5 100644 --- a/module/applications/ui/sceneNavigation.mjs +++ b/module/applications/ui/sceneNavigation.mjs @@ -31,7 +31,7 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi const environments = daggerheartInfo.sceneEnvironments.filter( x => x && x.testUserPermission(game.user, 'LIMITED') ); - const hasEnvironments = environments.length > 0; + const hasEnvironments = environments.length > 0 && x.isView; return { ...x, hasEnvironments, diff --git a/module/canvas/placeables/token.mjs b/module/canvas/placeables/token.mjs index d8acb73a..2266d0da 100644 --- a/module/canvas/placeables/token.mjs +++ b/module/canvas/placeables/token.mjs @@ -153,7 +153,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { textStyle.wordWrapWidth = this.w * 2.5; textStyle.fontStyle = 'italic'; - const helpText = new PreciseText( + const helpText = new foundry.canvas.containers.PreciseText( `(${game.i18n.localize('DAGGERHEART.UI.Tooltip.previewTokenHelp')})`, textStyle ); diff --git a/module/data/action/attackAction.mjs b/module/data/action/attackAction.mjs index ed97072f..7be7461d 100644 --- a/module/data/action/attackAction.mjs +++ b/module/data/action/attackAction.mjs @@ -36,6 +36,7 @@ export default class DHAttackAction extends DHDamageAction { async use(event, options) { const result = await super.use(event, options); + if (!result.message) return; if (result.message.system.action.roll?.type === 'attack') { const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns; diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index 482d2c41..cd26eb21 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -112,10 +112,12 @@ export default class DamageRoll extends DHRoll { const changeKeys = []; for (const roll of this.options.roll) { - for (const damageType of roll.damageTypes) changeKeys.push(`system.bonuses.${type}.${damageType}`); + for (const damageType of roll.damageTypes?.values?.() ?? []) { + changeKeys.push(`system.bonuses.${type}.${damageType}`); + } } - const item = this.data.parent.items?.get(this.options.source.item); + const item = this.data.parent?.items?.get(this.options.source.item); if (item) { switch (item.type) { case 'weapon': diff --git a/module/documents/tooltipManager.mjs b/module/documents/tooltipManager.mjs index dac5aea3..c4b52bb5 100644 --- a/module/documents/tooltipManager.mjs +++ b/module/documents/tooltipManager.mjs @@ -67,7 +67,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti if (item) { const isAction = item instanceof game.system.api.models.actions.actionsTypes.base; const isEffect = item instanceof ActiveEffect; - await this.enrichText(item, isAction || isEffect); + await this.enrichText(item); const type = isAction ? 'action' : isEffect ? 'effect' : item.type; html = await foundry.applications.handlebars.renderTemplate( @@ -202,10 +202,20 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti } } - async enrichText(item, flatStructure) { + async enrichText(item) { const { TextEditor } = foundry.applications.ux; + + if (item.system?.metadata?.hasDescription) { + const enrichedValue = + (await item.system?.getEnrichedDescription?.()) ?? + (await TextEditor.enrichHTML(item.system.description)); + foundry.utils.setProperty(item, 'system.enrichedDescription', enrichedValue); + } else if (item.description) { + const enrichedValue = await TextEditor.enrichHTML(item.description); + foundry.utils.setProperty(item, 'enrichedDescription', enrichedValue); + } + const enrichPaths = [ - { path: flatStructure ? '' : 'system', name: 'description' }, { path: 'system', name: 'features' }, { path: 'system', name: 'actions' }, { path: 'system', name: 'customActions' } diff --git a/module/systemRegistration/socket.mjs b/module/systemRegistration/socket.mjs index 82ca2e1c..a9e86917 100644 --- a/module/systemRegistration/socket.mjs +++ b/module/systemRegistration/socket.mjs @@ -93,10 +93,6 @@ export const registerSocketHooks = () => { } } }); - Hooks.on(socketEvent.RefreshDocument, async data => { - const document = await foundry.utils.fromUuid(data.uuid); - document.sheet.render(); - }); }; export const registerUserQueries = () => { diff --git a/templates/ui/itemBrowser/itemContainer.hbs b/templates/ui/itemBrowser/itemContainer.hbs index f6aefa6b..0040a692 100644 --- a/templates/ui/itemBrowser/itemContainer.hbs +++ b/templates/ui/itemBrowser/itemContainer.hbs @@ -10,7 +10,7 @@
- {{{system.description}}} + {{{system.enrichedDescription}}}
{{/each}} \ No newline at end of file From 9473250f0c80a3b410021b69002ceec60ffcf5d3 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Tue, 13 Jan 2026 20:47:43 +0100 Subject: [PATCH 06/48] Fixed critical threshold applying to reactions --- module/data/fields/action/rollField.mjs | 2 +- module/dice/d20Roll.mjs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/module/data/fields/action/rollField.mjs b/module/data/fields/action/rollField.mjs index e2196c1c..63d48990 100644 --- a/module/data/fields/action/rollField.mjs +++ b/module/data/fields/action/rollField.mjs @@ -87,7 +87,7 @@ export class DHActionRollData extends foundry.abstract.DataModel { if (this.type === CONFIG.DH.GENERAL.rollTypes.attack.id) modifiers.push({ label: 'Bonus to Hit', - value: this.bonus ?? this.parent.actor.system.attack.roll.bonus + value: this.bonus ?? this.parent.actor.system.attack.roll.bonus ?? 0 }); break; default: diff --git a/module/dice/d20Roll.mjs b/module/dice/d20Roll.mjs index f679d725..3ddd8027 100644 --- a/module/dice/d20Roll.mjs +++ b/module/dice/d20Roll.mjs @@ -35,7 +35,9 @@ export default class D20Roll extends DHRoll { get isCritical() { if (!this.d20._evaluated) return; - return this.d20.total >= this.data.system.criticalThreshold; + + const criticalThreshold = this.options.actionType === 'reaction' ? 20 : this.data.system.criticalThreshold; + return this.d20.total >= criticalThreshold; } get hasAdvantage() { From acafd2c8b923a543a0ef90504280e7ee261b6771 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Tue, 13 Jan 2026 21:00:24 +0100 Subject: [PATCH 07/48] Added fallback for defaultHopeDice and defaultFearDice --- module/dice/dualityRoll.mjs | 10 ++++++---- module/enrichers/DualityRollEnricher.mjs | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index 155d6aa5..32bb167a 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -131,11 +131,13 @@ export default class DualityRoll extends D20Roll { return; } - const { defaultHopeDice, defaultFearDice } = this.data.rules.dualityRoll; - - this.terms[0] = new foundry.dice.terms.Die({ faces: defaultHopeDice }); + this.terms[0] = new foundry.dice.terms.Die({ + faces: this.data.rules.dualityRoll?.defaultHopeDice ?? 12 + }); this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' }); - this.terms[2] = new foundry.dice.terms.Die({ faces: defaultFearDice }); + this.terms[2] = new foundry.dice.terms.Die({ + faces: this.data.rules.dualityRoll?.defaultFearDice ?? 12 + }); } applyAdvantage() { diff --git a/module/enrichers/DualityRollEnricher.mjs b/module/enrichers/DualityRollEnricher.mjs index 1d6404ff..95733c45 100644 --- a/module/enrichers/DualityRollEnricher.mjs +++ b/module/enrichers/DualityRollEnricher.mjs @@ -101,7 +101,7 @@ export const enrichedDualityRoll = async ( await target.diceRoll(config); } else { // For no target, call DualityRoll directly with basic data - config.data = { experiences: {}, traits: {} }; + config.data = { experiences: {}, traits: {}, rules: {} }; config.source = { actor: null }; await CONFIG.Dice.daggerheart.DualityRoll.build(config); } From 5b61340fee0bd73ced82dd646ded6f301f4c0061 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Tue, 13 Jan 2026 22:36:50 +0100 Subject: [PATCH 08/48] Fixed /dr without target failing resource update --- daggerheart.mjs | 2 +- module/dice/dualityRoll.mjs | 2 ++ module/enrichers/DualityRollEnricher.mjs | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/daggerheart.mjs b/daggerheart.mjs index f27892e2..4e88c148 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -309,7 +309,7 @@ Hooks.on('chatMessage', (_, message) => { target, difficulty, title, - label: 'test', + label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'), actionType: null, advantage }); diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index 32bb167a..aaca7400 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -261,6 +261,8 @@ export default class DualityRoll extends D20Roll { } static async handleTriggers(roll, config) { + if (!config.source?.actor) return; + const updates = []; const dualityUpdates = await game.system.registeredTriggers.runTrigger( CONFIG.DH.TRIGGER.triggers.dualityRoll.id, diff --git a/module/enrichers/DualityRollEnricher.mjs b/module/enrichers/DualityRollEnricher.mjs index 95733c45..536847f7 100644 --- a/module/enrichers/DualityRollEnricher.mjs +++ b/module/enrichers/DualityRollEnricher.mjs @@ -86,9 +86,9 @@ export const enrichedDualityRoll = async ( const config = { event: event ?? {}, title: title, + headerTitle: label, roll: { trait: traitValue && target ? traitValue : null, - label: label, difficulty: difficulty, advantage, type: reaction ? 'reaction' : null From 4aa414be0036d962115943a46948fd0e53ae975b Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Thu, 15 Jan 2026 00:53:33 +0100 Subject: [PATCH 09/48] [Fix] Daggerheart Menu LightMode (#1534) * Fixed so the menu and menuIcon coloration correctly changes with mode change * . * . * . --- styles/less/global/global.less | 11 +++++++++++ styles/less/ui/sidebar/tabs.less | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/styles/less/global/global.less b/styles/less/global/global.less index 6cc63c2a..6c63fe7a 100644 --- a/styles/less/global/global.less +++ b/styles/less/global/global.less @@ -51,3 +51,14 @@ } } } + +/* TODO: Remove me when this issue is resolved https://github.com/foundryvtt/foundryvtt/issues/13734 */ +body.theme-dark, +.themed.theme-dark { + color-scheme: dark; +} + +body.theme-light, +.themed.theme-light { + color-scheme: light; +} diff --git a/styles/less/ui/sidebar/tabs.less b/styles/less/ui/sidebar/tabs.less index e9de2924..c620ff91 100644 --- a/styles/less/ui/sidebar/tabs.less +++ b/styles/less/ui/sidebar/tabs.less @@ -1,4 +1,4 @@ -.theme-light #interface #ui-right #sidebar { +.theme-light#interface #ui-right #sidebar { menu li button img { filter: @grey-filter; } From b92a47406980f9d4e9f4d195654fffe59b39ef62 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Wed, 14 Jan 2026 20:04:00 -0500 Subject: [PATCH 10/48] Fix bottom gap of item settings and make header rows more rhythmic (#1529) --- styles/less/global/index.less | 1 - styles/less/global/item-header.less | 2 +- styles/less/global/tab-settings.less | 8 -------- 3 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 styles/less/global/tab-settings.less diff --git a/styles/less/global/index.less b/styles/less/global/index.less index f51140de..216dc9f4 100644 --- a/styles/less/global/index.less +++ b/styles/less/global/index.less @@ -10,7 +10,6 @@ @import './tab-description.less'; @import './tab-features.less'; @import './tab-effects.less'; -@import './tab-settings.less'; @import './item-header.less'; @import './feature-section.less'; @import './inventory-item.less'; diff --git a/styles/less/global/item-header.less b/styles/less/global/item-header.less index 073762e0..f47ca7dc 100755 --- a/styles/less/global/item-header.less +++ b/styles/less/global/item-header.less @@ -160,7 +160,7 @@ .item-description { display: flex; flex-direction: column; - gap: 10px; + gap: 7px; } h3 { diff --git a/styles/less/global/tab-settings.less b/styles/less/global/tab-settings.less deleted file mode 100644 index 3d5248be..00000000 --- a/styles/less/global/tab-settings.less +++ /dev/null @@ -1,8 +0,0 @@ -@import '../utils/colors.less'; -@import '../utils/fonts.less'; - -.sheet.daggerheart.dh-style { - .tab.settings { - margin-bottom: 36px; - } -} From 9393bab6cf6cd1203d4f991895d3e589d011a104 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Thu, 15 Jan 2026 09:45:10 +0100 Subject: [PATCH 11/48] Added so that beastforms can set a scale to handle specific images (#1532) --- lang/en.json | 3 ++- module/data/activeEffect/beastformEffect.mjs | 9 +++++++-- module/data/item/beastform.mjs | 6 +++++- templates/sheets/items/beastform/settings.hbs | 3 +++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/lang/en.json b/lang/en.json index b84906f3..14915a81 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2284,7 +2284,8 @@ "placeholder": "Using character dimensions", "disabledPlaceholder": "Set by character size", "height": { "label": "Height" }, - "width": { "label": "Width" } + "width": { "label": "Width" }, + "scale": { "label": "Token Scale" } }, "evolved": { "maximumTier": { "label": "Maximum Tier" }, diff --git a/module/data/activeEffect/beastformEffect.mjs b/module/data/activeEffect/beastformEffect.mjs index 5311b827..65e36606 100644 --- a/module/data/activeEffect/beastformEffect.mjs +++ b/module/data/activeEffect/beastformEffect.mjs @@ -19,6 +19,7 @@ export default class BeastformEffect extends BaseEffect { base64: false }), tokenSize: new fields.SchemaField({ + scale: new fields.NumberField({ nullable: false, initial: 1 }), height: new fields.NumberField({ integer: false, nullable: true }), width: new fields.NumberField({ integer: false, nullable: true }) }) @@ -55,7 +56,9 @@ export default class BeastformEffect extends BaseEffect { const update = { ...baseUpdate, texture: { - src: this.characterTokenData.tokenImg + src: this.characterTokenData.tokenImg, + scaleX: this.characterTokenData.tokenSize.scale, + scaleY: this.characterTokenData.tokenSize.scale }, ring: { enabled: this.characterTokenData.usesDynamicToken, @@ -86,7 +89,9 @@ export default class BeastformEffect extends BaseEffect { y, 'texture': { enabled: this.characterTokenData.usesDynamicToken, - src: token.flags.daggerheart?.beastformTokenImg ?? this.characterTokenData.tokenImg + src: token.flags.daggerheart?.beastformTokenImg ?? this.characterTokenData.tokenImg, + scaleX: this.characterTokenData.tokenSize.scale, + scaleY: this.characterTokenData.tokenSize.scale }, 'ring': { subject: { diff --git a/module/data/item/beastform.mjs b/module/data/item/beastform.mjs index 1840e26a..dd491169 100644 --- a/module/data/item/beastform.mjs +++ b/module/data/item/beastform.mjs @@ -49,6 +49,7 @@ export default class DHBeastform extends BaseDataItem { choices: CONFIG.DH.ACTOR.tokenSize, initial: CONFIG.DH.ACTOR.tokenSize.custom.id }), + scale: new fields.NumberField({ nullable: false, min: 0.2, max: 3, step: 0.05, initial: 1 }), height: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }), width: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }) }), @@ -184,6 +185,7 @@ export default class DHBeastform extends BaseDataItem { tokenImg: this.parent.parent.prototypeToken.texture.src, tokenRingImg: this.parent.parent.prototypeToken.ring.subject.texture, tokenSize: { + scale: this.parent.parent.prototypeToken.texture.scaleX, height: this.parent.parent.prototypeToken.height, width: this.parent.parent.prototypeToken.width } @@ -209,7 +211,9 @@ export default class DHBeastform extends BaseDataItem { height, width, texture: { - src: this.tokenImg + src: this.tokenImg, + scaleX: this.tokenSize.scale, + scaleY: this.tokenSize.scale }, ring: { subject: { diff --git a/templates/sheets/items/beastform/settings.hbs b/templates/sheets/items/beastform/settings.hbs index 844b9d61..82065ad3 100644 --- a/templates/sheets/items/beastform/settings.hbs +++ b/templates/sheets/items/beastform/settings.hbs @@ -47,6 +47,9 @@ disabled=dimensionsDisabled }} +
+ {{formGroup systemFields.tokenSize.fields.scale value=source.system.tokenSize.scale localize=true }} +
{{else}} {{localize "DAGGERHEART.ITEMS.Beastform.evolvedTokenHint"}} {{/unless}} From e590dde5679989610f3215f3f8f9d7758faa490d Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Thu, 15 Jan 2026 22:37:03 +0100 Subject: [PATCH 12/48] Fixed overflow for the menu (#1537) --- module/applications/sidebar/tabs/daggerheartMenu.mjs | 2 +- styles/less/ui/sidebar/daggerheartMenu.less | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/module/applications/sidebar/tabs/daggerheartMenu.mjs b/module/applications/sidebar/tabs/daggerheartMenu.mjs index 6c7a9df1..b29437bf 100644 --- a/module/applications/sidebar/tabs/daggerheartMenu.mjs +++ b/module/applications/sidebar/tabs/daggerheartMenu.mjs @@ -25,7 +25,7 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract /** @override */ static DEFAULT_OPTIONS = { - classes: ['dh-style'], + classes: ['dh-style', 'directory'], window: { title: 'SIDEBAR.TabSettings' }, diff --git a/styles/less/ui/sidebar/daggerheartMenu.less b/styles/less/ui/sidebar/daggerheartMenu.less index 80eda9a1..677214d7 100644 --- a/styles/less/ui/sidebar/daggerheartMenu.less +++ b/styles/less/ui/sidebar/daggerheartMenu.less @@ -5,6 +5,8 @@ display: flex; flex-direction: column; gap: 8px; + overflow: auto; + height: 100%; } h2 { From 8d9b954629e2fdeb42ea9593348790a9199a200f Mon Sep 17 00:00:00 2001 From: WBHarry Date: Fri, 16 Jan 2026 00:10:48 +0100 Subject: [PATCH 13/48] Fixed missing homebrew settings tab translation --- lang/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/lang/en.json b/lang/en.json index 14915a81..1157c7b9 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2085,6 +2085,7 @@ "tier4": "tier 4", "domains": "Domains", "downtime": "Downtime", + "itemFeatures": "Item Features", "roll": "Roll", "rules": "Rules", "partyMembers": "Party Members", From d94eb9dcbd7f42dff0b45dfb509e9a05a29adbfc Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Fri, 16 Jan 2026 00:12:41 +0100 Subject: [PATCH 14/48] Fixed so that reaction button is shown again (#1540) --- module/applications/dialogs/d20RollDialog.mjs | 2 +- system.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index d8306923..441842dc 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -123,7 +123,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio context.formula = this.roll.constructFormula(this.config); if (this.actor?.system?.traits) context.abilities = this.getTraitModifiers(); - context.showReaction = !this.config.roll?.type && context.rollType === 'DualityRoll'; + context.showReaction = !this.config.roll?.type || context.rollType === 'DualityRoll'; context.reactionOverride = this.reactionOverride; } diff --git a/system.json b/system.json index 43f82f06..50b4cd2d 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.5.0", + "version": "1.5.1", "compatibility": { "minimum": "13.346", "verified": "13.351", From 4cd6fe58daae09f5f910b74a3e91cb80dd83c9a7 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Thu, 15 Jan 2026 18:55:36 -0500 Subject: [PATCH 15/48] Fix chat log theme to inherit interface theme (#1544) --- module/applications/ui/chatLog.mjs | 13 +++++++++++++ styles/less/global/chat.less | 22 ++++++++++------------ styles/less/ui/chat/ability-use.less | 1 + styles/less/ui/chat/action.less | 1 + styles/less/ui/chat/chat.less | 4 ++-- styles/less/ui/chat/damage-summary.less | 1 + styles/less/ui/chat/downtime.less | 1 + styles/less/ui/chat/effect-summary.less | 1 + styles/less/ui/chat/group-roll.less | 4 ++-- styles/less/ui/chat/sheet.less | 1 + 10 files changed, 33 insertions(+), 16 deletions(-) diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index 20dfea8d..c4a313fa 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -92,6 +92,19 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo super.close(options); } + /** Ensure the chat theme inherits the interface theme */ + _replaceHTML(result, content, options) { + const themedElement = result.log?.querySelector(".chat-log"); + themedElement?.classList.remove("themed", "theme-light", "theme-dark"); + super._replaceHTML(result, content, options); + } + + /** Remove chat log theme from notifications area */ + async _onFirstRender(result, content) { + await super._onFirstRender(result, content); + document.querySelector("#chat-notifications .chat-log")?.classList.remove("themed", "theme-light", "theme-dark") + } + async onRollSimple(event, message) { const buttonType = event.target.dataset.type ?? 'damage', total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0), diff --git a/styles/less/global/chat.less b/styles/less/global/chat.less index 69ee369a..dc671e44 100644 --- a/styles/less/global/chat.less +++ b/styles/less/global/chat.less @@ -2,20 +2,18 @@ @import '../utils/fonts.less'; @import '../utils/mixin.less'; -.theme-light { - .daggerheart.chat-sidebar .chat-log, - #chat-notifications .chat-log { - .chat-message { - background-image: url('../assets/parchments/dh-parchment-light.png'); +.daggerheart.chat-sidebar.theme-light, +#interface.theme-light { + .chat-log .chat-message { + background-image: url('../assets/parchments/dh-parchment-light.png'); - .message-header .message-header-metadata .message-metadata, - .message-header .message-header-main .message-sub-header-container { - color: @dark; - } + .message-header .message-header-metadata .message-metadata, + .message-header .message-header-main .message-sub-header-container { + color: @dark; + } - .message-header .message-header-main .message-sub-header-container h4 { - color: @dark-blue; - } + .message-header .message-header-main .message-sub-header-container h4 { + color: @dark-blue; } } } diff --git a/styles/less/ui/chat/ability-use.less b/styles/less/ui/chat/ability-use.less index 88302d0d..b590911d 100644 --- a/styles/less/ui/chat/ability-use.less +++ b/styles/less/ui/chat/ability-use.less @@ -2,6 +2,7 @@ @import '../../utils/fonts.less'; @import '../../utils/spacing.less'; +.daggerheart.chat-sidebar.theme-light, #interface.theme-light { .daggerheart.chat.domain-card { .domain-card-move .domain-card-header { diff --git a/styles/less/ui/chat/action.less b/styles/less/ui/chat/action.less index 8d309cfe..a3d2f3cc 100644 --- a/styles/less/ui/chat/action.less +++ b/styles/less/ui/chat/action.less @@ -2,6 +2,7 @@ @import '../../utils/fonts.less'; @import '../../utils/spacing.less'; +.daggerheart.chat-sidebar.theme-light, #interface.theme-light { .daggerheart.chat.action { .action-move .action-section { diff --git a/styles/less/ui/chat/chat.less b/styles/less/ui/chat/chat.less index 6f0e5e85..57e9fd57 100644 --- a/styles/less/ui/chat/chat.less +++ b/styles/less/ui/chat/chat.less @@ -2,9 +2,9 @@ @import '../../utils/fonts.less'; @import '../../utils/spacing.less'; +.daggerheart.chat-sidebar.theme-light, #interface.theme-light { - .daggerheart.chat-sidebar .chat-log, - #chat-notifications .chat-log { + .chat-log { --text-color: @dark-blue; --bg-color: @dark-blue-40; diff --git a/styles/less/ui/chat/damage-summary.less b/styles/less/ui/chat/damage-summary.less index 3fea45e5..b47cd41f 100644 --- a/styles/less/ui/chat/damage-summary.less +++ b/styles/less/ui/chat/damage-summary.less @@ -1,5 +1,6 @@ @import '../../utils/colors.less'; +.daggerheart.chat-sidebar.theme-light, #interface.theme-light { .daggerheart.chat.damage-summary .token-target-container { &:hover { diff --git a/styles/less/ui/chat/downtime.less b/styles/less/ui/chat/downtime.less index 2875ea10..ca0cd090 100644 --- a/styles/less/ui/chat/downtime.less +++ b/styles/less/ui/chat/downtime.less @@ -2,6 +2,7 @@ @import '../../utils/fonts.less'; @import '../../utils/spacing.less'; +.daggerheart.chat-sidebar.theme-light, #interface.theme-light { .daggerheart.chat.downtime { .downtime-moves-list .downtime-move { diff --git a/styles/less/ui/chat/effect-summary.less b/styles/less/ui/chat/effect-summary.less index 3d72571d..87d53eeb 100644 --- a/styles/less/ui/chat/effect-summary.less +++ b/styles/less/ui/chat/effect-summary.less @@ -1,5 +1,6 @@ @import '../../utils/colors.less'; +.daggerheart.chat-sidebar.theme-light, #interface.theme-light { .daggerheart.chat.effect-summary { .effect-header, diff --git a/styles/less/ui/chat/group-roll.less b/styles/less/ui/chat/group-roll.less index 02b8e312..9ed87220 100644 --- a/styles/less/ui/chat/group-roll.less +++ b/styles/less/ui/chat/group-roll.less @@ -125,9 +125,9 @@ .group-roll-trait { padding: 2px 8px; - border: 1px solid light-dark(white, white); + border: 1px solid light-dark(@dark-blue, white); border-radius: 6px; - color: light-dark(white, white); + color: light-dark(@dark-blue, white); background: light-dark(@beige-80, @beige-80); } } diff --git a/styles/less/ui/chat/sheet.less b/styles/less/ui/chat/sheet.less index 3d47a9b5..b632db35 100644 --- a/styles/less/ui/chat/sheet.less +++ b/styles/less/ui/chat/sheet.less @@ -1,6 +1,7 @@ @import '../../utils/colors.less'; @import '../../utils/fonts.less'; +.daggerheart.chat-sidebar.theme-light, #interface.theme-light { .chat-message:not(.duality) .message-content { color: @dark; From fad09a1b3a73c0fe932da14541732fcc894d8d24 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:43:25 +0100 Subject: [PATCH 16/48] [Feature] 1541 - Trigger Improvements (#1542) * Improved registration and unregistration of triggers * Added logging * Fixed Feature level unregistration * Fixed action deletion unregistration * SceneEnvironment stub * Update module/data/registeredTriggers.mjs Co-authored-by: Carlos Fernandez --------- Co-authored-by: Carlos Fernandez --- daggerheart.mjs | 56 ++--------- lang/en.json | 3 + module/data/_module.mjs | 1 + module/data/item/base.mjs | 43 ++++---- module/data/registeredTriggers.mjs | 154 +++++++++++++++++++++++++++++ module/documents/actor.mjs | 10 ++ module/documents/item.mjs | 19 ++++ module/documents/token.mjs | 6 ++ 8 files changed, 226 insertions(+), 66 deletions(-) create mode 100644 module/data/registeredTriggers.mjs diff --git a/daggerheart.mjs b/daggerheart.mjs index 4e88c148..3abcd210 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -78,6 +78,7 @@ CONFIG.ux.ContextMenu = applications.ux.DHContextMenu; CONFIG.ux.TooltipManager = documents.DhTooltipManager; CONFIG.ux.TemplateManager = new TemplateManager(); CONFIG.ux.TokenManager = new TokenManager(); +CONFIG.debug.triggers = false; Hooks.once('init', () => { game.system.api = { @@ -89,7 +90,7 @@ Hooks.once('init', () => { fields }; - game.system.registeredTriggers = new RegisteredTriggers(); + game.system.registeredTriggers = new game.system.api.data.RegisteredTriggers(); const { DocumentSheetConfig } = foundry.applications.apps; DocumentSheetConfig.unregisterSheet(TokenDocument, 'core', foundry.applications.sheets.TokenConfig); @@ -389,49 +390,12 @@ Hooks.on('refreshToken', (_, options) => { Hooks.on('renderCompendiumDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html)); Hooks.on('renderDocumentDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html)); -class RegisteredTriggers extends Map { - constructor() { - super(); - } +/* Non actor-linked Actors should unregister the triggers of their tokens if a scene's token layer is torn down */ +Hooks.on('canvasTearDown', canvas => { + game.system.registeredTriggers.unregisterSceneTriggers(canvas.scene); +}); - async registerTriggers(trigger, actor, triggeringActorType, uuid, commands) { - const existingTrigger = this.get(trigger); - if (!existingTrigger) this.set(trigger, new Map()); - - this.get(trigger).set(uuid, { actor, triggeringActorType, commands }); - } - - async runTrigger(trigger, currentActor, ...args) { - const updates = []; - const triggerSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).triggers; - if (!triggerSettings.enabled) return updates; - - const dualityTrigger = this.get(trigger); - if (dualityTrigger) { - for (let { actor, triggeringActorType, commands } of dualityTrigger.values()) { - const triggerData = CONFIG.DH.TRIGGER.triggers[trigger]; - if (triggerData.usesActor && triggeringActorType !== 'any') { - if (triggeringActorType === 'self' && currentActor?.uuid !== actor) continue; - else if (triggeringActorType === 'other' && currentActor?.uuid === actor) continue; - } - - for (let command of commands) { - try { - const result = await command(...args); - if (result?.updates?.length) updates.push(...result.updates); - } catch (_) { - const triggerName = game.i18n.localize(triggerData.label); - ui.notifications.error( - game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerError', { - trigger: triggerName, - actor: currentActor?.name - }) - ); - } - } - } - } - - return updates; - } -} +/* Non actor-linked Actors should register the triggers of their tokens on a readied scene */ +Hooks.on('canvasReady', canas => { + game.system.registeredTriggers.registerSceneTriggers(canvas.scene); +}); diff --git a/lang/en.json b/lang/en.json index 1157c7b9..8e64ab7d 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2710,6 +2710,9 @@ "rerollDamage": "Reroll Damage", "assignTagRoll": "Assign as Tag Roll" }, + "ConsoleLogs": { + "triggerRun": "DH TRIGGER | Item '{item}' on actor '{actor}' ran a '{trigger}' trigger." + }, "Countdowns": { "title": "Countdowns", "toggleIconMode": "Toggle Icon Only", diff --git a/module/data/_module.mjs b/module/data/_module.mjs index 0a476ee9..7ad20808 100644 --- a/module/data/_module.mjs +++ b/module/data/_module.mjs @@ -1,6 +1,7 @@ export { default as DhCombat } from './combat.mjs'; export { default as DhCombatant } from './combatant.mjs'; export { default as DhTagTeamRoll } from './tagTeamRoll.mjs'; +export { default as RegisteredTriggers } from './registeredTriggers.mjs'; export * as countdowns from './countdowns.mjs'; export * as actions from './action/_module.mjs'; diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 415fc8d4..0c9fdabe 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -164,26 +164,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { prepareBaseData() { super.prepareBaseData(); - - for (const action of this.actions ?? []) { - if (!action.actor) continue; - - const actionsToRegister = []; - for (let i = 0; i < action.triggers.length; i++) { - const trigger = action.triggers[i]; - const { args } = CONFIG.DH.TRIGGER.triggers[trigger.trigger]; - const fn = new foundry.utils.AsyncFunction(...args, `{${trigger.command}\n}`); - actionsToRegister.push(fn.bind(action)); - if (i === action.triggers.length - 1) - game.system.registeredTriggers.registerTriggers( - trigger.trigger, - action.actor?.uuid, - trigger.triggeringActorType, - this.parent.uuid, - actionsToRegister - ); - } - } + game.system.registeredTriggers.registerItemTriggers(this.parent); } async _preCreate(data, options, user) { @@ -246,6 +227,28 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { const armorData = getScrollTextData(this.parent.parent.system.resources, changed.system.marks, 'armor'); options.scrollingTextData = [armorData]; } + + if (changed.system?.actions) { + const triggersToRemove = Object.keys(changed.system.actions).reduce((acc, key) => { + if (!changed.system.actions[key]) { + const strippedKey = key.replace('-=', ''); + acc.push(...this.actions.get(strippedKey).triggers.map(x => x.trigger)); + } + + return acc; + }, []); + + game.system.registeredTriggers.unregisterTriggers(triggersToRemove, this.parent.uuid); + + if (!(this.parent.parent.token instanceof game.system.api.documents.DhToken)) { + for (const token of this.parent.parent.getActiveTokens()) { + game.system.registeredTriggers.unregisterTriggers( + triggersToRemove, + `${token.document.uuid}.${this.parent.uuid}` + ); + } + } + } } _onUpdate(changed, options, userId) { diff --git a/module/data/registeredTriggers.mjs b/module/data/registeredTriggers.mjs new file mode 100644 index 00000000..fe962c5e --- /dev/null +++ b/module/data/registeredTriggers.mjs @@ -0,0 +1,154 @@ +export default class RegisteredTriggers extends Map { + constructor() { + super(); + } + + registerTriggers(triggers, actor, uuid) { + for (const triggerKey of Object.keys(CONFIG.DH.TRIGGER.triggers)) { + const match = triggers[triggerKey]; + const existingTrigger = this.get(triggerKey); + + if (!match) { + if (existingTrigger?.get(uuid)) this.get(triggerKey).delete(uuid); + } else { + const { trigger, triggeringActorType, commands } = match; + + if (!existingTrigger) this.set(trigger, new Map()); + this.get(trigger).set(uuid, { actor, triggeringActorType, commands }); + } + } + } + + registerItemTriggers(item, registerOverride) { + for (const action of item.system.actions ?? []) { + if (!action.actor) continue; + + /* Non actor-linked should only prep synthetic actors so they're not registering triggers unless they're on the canvas */ + if ( + !registerOverride && + !action.actor.prototypeToken.actorLink && + (!(action.actor.parent instanceof game.system.api.documents.DhToken) || !action.actor.parent?.uuid) + ) + continue; + + const triggers = {}; + for (const trigger of action.triggers) { + const { args } = CONFIG.DH.TRIGGER.triggers[trigger.trigger]; + const fn = new foundry.utils.AsyncFunction(...args, `{${trigger.command}\n}`); + + if (!triggers[trigger.trigger]) + triggers[trigger.trigger] = { + trigger: trigger.trigger, + triggeringActorType: trigger.triggeringActorType, + commands: [] + }; + triggers[trigger.trigger].commands.push(fn.bind(action)); + } + + this.registerTriggers(triggers, action.actor?.uuid, item.uuid); + } + } + + unregisterTriggers(triggerKeys, uuid) { + for (const triggerKey of triggerKeys) { + const existingTrigger = this.get(triggerKey); + if (!existingTrigger) return; + + existingTrigger.delete(uuid); + } + } + + unregisterItemTriggers(items) { + for (const item of items) { + if (!item.system.actions.size) continue; + + const triggers = (item.system.actions ?? []).reduce((acc, action) => { + acc.push(...action.triggers.map(x => x.trigger)); + return acc; + }, []); + + this.unregisterTriggers(triggers, item.uuid); + } + } + + unregisterSceneTriggers(scene) { + 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); + } + this.set(triggerKey, filtered); + } + } + + 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); + // } + // } + + for (const actor of scene.tokens.filter(x => x.actor).map(x => x.actor)) { + if (actor.prototypeToken.actorLink) continue; + + for (const item of actor.items) { + this.registerItemTriggers(item); + } + } + } + + async runTrigger(trigger, currentActor, ...args) { + const updates = []; + const triggerSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).triggers; + if (!triggerSettings.enabled) return updates; + + const dualityTrigger = this.get(trigger); + if (dualityTrigger) { + const tokenBoundActors = ['adversary', 'environment']; + const triggerActors = ['character', ...tokenBoundActors]; + 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') { + if (triggeringActorType === 'self' && currentActor?.uuid !== actorUuid) continue; + else if (triggeringActorType === 'other' && currentActor?.uuid === actorUuid) continue; + } + + for (const command of commands) { + try { + if (CONFIG.debug.triggers) { + const item = await foundry.utils.fromUuid(itemUuid); + console.log( + game.i18n.format('DAGGERHEART.UI.ConsoleLogs.triggerRun', { + actor: actor.name ?? '', + item: item?.name ?? '', + trigger: game.i18n.localize(triggerData.label) + }) + ); + } + + const result = await command(...args); + if (result?.updates?.length) updates.push(...result.updates); + } catch (_) { + const triggerName = game.i18n.localize(triggerData.label); + ui.notifications.error( + game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerError', { + trigger: triggerName, + actor: currentActor?.name + }) + ); + } + } + } + } + + return updates; + } +} diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 1a4153ad..27c310ae 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -104,6 +104,16 @@ export default class DhpActor extends Actor { } } + async _preDelete() { + if (this.prototypeToken.actorLink) { + game.system.registeredTriggers.unregisterItemTriggers(this.items); + } else { + for (const token of this.getActiveTokens()) { + game.system.registeredTriggers.unregisterItemTriggers(token.actor.items); + } + } + } + _onDelete(options, userId) { super._onDelete(options, userId); for (const party of this.parties) { diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 7607658c..0a163dab 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -208,4 +208,23 @@ export default class DHItem extends foundry.documents.Item { cls.create(msg); } + + deleteTriggers() { + const actions = Array.from(this.system.actions ?? []); + if (!actions.length) return; + + const triggerKeys = actions.flatMap(action => action.triggers.map(x => x.trigger)); + + game.system.registeredTriggers.unregisterTriggers(triggerKeys, this.uuid); + + if (!(this.actor.parent instanceof game.system.api.documents.DhToken)) { + for (const token of this.actor.getActiveTokens()) { + game.system.registeredTriggers.unregisterTriggers(triggerKeys, `${token.document.uuid}.${this.uuid}`); + } + } + } + + async _preDelete() { + this.deleteTriggers(); + } } diff --git a/module/documents/token.mjs b/module/documents/token.mjs index 4ac29264..317f3acf 100644 --- a/module/documents/token.mjs +++ b/module/documents/token.mjs @@ -536,4 +536,10 @@ export default class DHToken extends CONFIG.Token.documentClass { }; } //#endregion + + async _preDelete() { + if (this.actor && !this.actor.prototypeToken?.actorLink) { + game.system.registeredTriggers.unregisterItemTriggers(this.actor.items); + } + } } From 822c522f61baa6460963bde60ea7627712eddf0e Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Fri, 16 Jan 2026 09:35:02 +0100 Subject: [PATCH 17/48] [Fix] 1535 - Toggle Bonuses (#1538) * Fixed all SRD instances of Powerful and Massive * Fixed suppressed effects being added to roll formula options * Fixed weapon effects being presented when it's no the weapon itself * . * . * Fixed secondary weapons effects * Raised system version * Update module/data/action/baseAction.mjs Co-authored-by: Carlos Fernandez * Update module/documents/chatMessage.mjs Co-authored-by: Carlos Fernandez --------- Co-authored-by: Carlos Fernandez --- .../applications/sheets/actors/character.mjs | 2 +- .../sheets/api/application-mixin.mjs | 5 +++- module/data/action/baseAction.mjs | 28 +++++++++++++------ module/documents/chatMessage.mjs | 4 ++- ..._Advanced_Greatstaff_4UzxqfkwF8gDSdu7.json | 13 +-------- ..._Advanced_Greatsword_MAC6YWTo4lzSotQc.json | 10 ------- .../weapon_Double_Flail_xm1yU7k58fMgXxRR.json | 13 +-------- .../weapon_Elder_Bow_JdWcn9W1edhAEInL.json | 13 +-------- ...Floating_Bladeshards_3vti3xfo0wJND7ew.json | 13 +-------- ...apon_Gilded_Falchion_VwcOgqnzjf9LBj2S.json | 13 +-------- .../weapon_Greatbow_MXBpbqQsZFln4rZk.json | 13 +-------- .../weapon_Greatstaff_Yk8pTEmyLLi4095S.json | 13 +-------- .../weapon_Greatsword_70ysaFJDREwTgvZa.json | 10 ------- ..._Improved_Greatstaff_LCuTrYXi4lhg6LqW.json | 13 +-------- ..._Improved_Greatsword_FPX4ouDrxXiQ5MDf.json | 10 ------- ...Legendary_Greatstaff_jDtvEabkHY1GFgfc.json | 13 +-------- ...Legendary_Greatsword_zMZ46F9VR7zdTxb9.json | 10 ------- .../weapon_Mage_Orb_XKBmBUEoGLdLcuqQ.json | 13 +-------- 18 files changed, 38 insertions(+), 171 deletions(-) diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index dd5f35fc..806d7a45 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -712,7 +712,7 @@ export default class CharacterSheet extends DHBaseActorSheet { headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { ability: abilityLabel }), - effects: Array.from(await this.document.allApplicableEffects()), + effects: await await game.system.api.data.actions.actionsTypes.base.getEffects(this.document), roll: { trait: button.dataset.attribute, type: 'trait' diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 7276316f..b590de86 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -505,7 +505,10 @@ export default function DHApplicationMixin(Base) { const doc = await getDocFromElement(target), action = doc?.system?.attack ?? doc; const config = action.prepareConfig(event); - config.effects = Array.from(await this.document.allApplicableEffects()); + config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects( + this.document, + doc + ); config.hasRoll = false; return action && action.workflow.get('damage').execute(config, null, true); } diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index dac4cf68..0addc6b3 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -198,7 +198,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel let config = this.prepareConfig(event); if (!config) return; - await this.addEffects(config); + config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(this.actor, this.item); if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return; @@ -266,14 +266,26 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel return config; } - /** */ - async addEffects(config) { - let effects = []; - if (this.actor) { - effects = Array.from(await this.actor.allApplicableEffects()); - } + /** + * Get the all potentially applicable effects on the actor + * @param {DHActor} actor The actor performing the action + * @param {DHItem|DhActor} effectParent The parent of the effect + * @returns {DhActiveEffect[]} + */ + static async getEffects(actor, effectParent) { + if (!actor) return []; + + return Array.from(await actor.allApplicableEffects()).filter(effect => { + /* Effects on weapons only ever apply for the weapon itself */ + if (effect.parent.type === 'weapon') { + /* Unless they're secondary - then they apply only to other primary weapons */ + if (effect.parent.system.secondary) { + if (effectParent.type !== 'weapon' || effectParent.system.secondary) return false; + } else if (effectParent?.id !== effect.parent.id) return false; + } - config.effects = effects; + return !effect.isSuppressed; + }); } /** diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index d85bcb45..c965f2e5 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -158,7 +158,9 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { const config = foundry.utils.deepClone(this.system); config.event = event; if (this.system.action) { - await this.system.action.addEffects(config); + const actor = await foundry.utils.fromUuid(config.source.actor); + const item = actor?.items.get(config.source.item) ?? null; + config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(actor, item); await this.system.action.workflow.get('damage')?.execute(config, this._id, true); } diff --git a/src/packs/items/weapons/weapon_Advanced_Greatstaff_4UzxqfkwF8gDSdu7.json b/src/packs/items/weapons/weapon_Advanced_Greatstaff_4UzxqfkwF8gDSdu7.json index 6ce54823..c66354c2 100644 --- a/src/packs/items/weapons/weapon_Advanced_Greatstaff_4UzxqfkwF8gDSdu7.json +++ b/src/packs/items/weapons/weapon_Advanced_Greatstaff_4UzxqfkwF8gDSdu7.json @@ -113,18 +113,7 @@ "name": "Powerful", "description": "On a successful attack, roll an additional damage die and discard the lowest result.", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "changes": [ - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" - } - ], + "changes": [], "_id": "sGVVxSM68Fmr1sSM", "type": "base", "system": {}, diff --git a/src/packs/items/weapons/weapon_Advanced_Greatsword_MAC6YWTo4lzSotQc.json b/src/packs/items/weapons/weapon_Advanced_Greatsword_MAC6YWTo4lzSotQc.json index fe3fff0e..71226630 100644 --- a/src/packs/items/weapons/weapon_Advanced_Greatsword_MAC6YWTo4lzSotQc.json +++ b/src/packs/items/weapons/weapon_Advanced_Greatsword_MAC6YWTo4lzSotQc.json @@ -118,16 +118,6 @@ "key": "system.evasion", "mode": 2, "value": "-1" - }, - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" } ], "_id": "E0PjC15OP55vIype", diff --git a/src/packs/items/weapons/weapon_Double_Flail_xm1yU7k58fMgXxRR.json b/src/packs/items/weapons/weapon_Double_Flail_xm1yU7k58fMgXxRR.json index 2e00f9c1..a118b399 100644 --- a/src/packs/items/weapons/weapon_Double_Flail_xm1yU7k58fMgXxRR.json +++ b/src/packs/items/weapons/weapon_Double_Flail_xm1yU7k58fMgXxRR.json @@ -113,18 +113,7 @@ "name": "Powerful", "description": "On a successful attack, roll an additional damage die and discard the lowest result.", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "changes": [ - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" - } - ], + "changes": [], "_id": "DCie5eR1dZH2Qvln", "type": "base", "system": {}, diff --git a/src/packs/items/weapons/weapon_Elder_Bow_JdWcn9W1edhAEInL.json b/src/packs/items/weapons/weapon_Elder_Bow_JdWcn9W1edhAEInL.json index b6437781..35659402 100644 --- a/src/packs/items/weapons/weapon_Elder_Bow_JdWcn9W1edhAEInL.json +++ b/src/packs/items/weapons/weapon_Elder_Bow_JdWcn9W1edhAEInL.json @@ -113,18 +113,7 @@ "name": "Powerful", "description": "On a successful attack, roll an additional damage die and discard the lowest result.", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "changes": [ - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" - } - ], + "changes": [], "_id": "sZ1XotFlGdkPPDG4", "type": "base", "system": {}, diff --git a/src/packs/items/weapons/weapon_Floating_Bladeshards_3vti3xfo0wJND7ew.json b/src/packs/items/weapons/weapon_Floating_Bladeshards_3vti3xfo0wJND7ew.json index fa7b7d45..232f26e9 100644 --- a/src/packs/items/weapons/weapon_Floating_Bladeshards_3vti3xfo0wJND7ew.json +++ b/src/packs/items/weapons/weapon_Floating_Bladeshards_3vti3xfo0wJND7ew.json @@ -113,18 +113,7 @@ "name": "Powerful", "description": "On a successful attack, roll an additional damage die and discard the lowest result.", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "changes": [ - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" - } - ], + "changes": [], "_id": "T831j6kZiMnpMNmv", "type": "base", "system": {}, diff --git a/src/packs/items/weapons/weapon_Gilded_Falchion_VwcOgqnzjf9LBj2S.json b/src/packs/items/weapons/weapon_Gilded_Falchion_VwcOgqnzjf9LBj2S.json index 551dcf56..ee8afebc 100644 --- a/src/packs/items/weapons/weapon_Gilded_Falchion_VwcOgqnzjf9LBj2S.json +++ b/src/packs/items/weapons/weapon_Gilded_Falchion_VwcOgqnzjf9LBj2S.json @@ -113,18 +113,7 @@ "name": "Powerful", "description": "On a successful attack, roll an additional damage die and discard the lowest result.", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "changes": [ - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" - } - ], + "changes": [], "_id": "ir4iKLIQ4CH1Qckn", "type": "base", "system": {}, diff --git a/src/packs/items/weapons/weapon_Greatbow_MXBpbqQsZFln4rZk.json b/src/packs/items/weapons/weapon_Greatbow_MXBpbqQsZFln4rZk.json index f97e5432..f56e77c7 100644 --- a/src/packs/items/weapons/weapon_Greatbow_MXBpbqQsZFln4rZk.json +++ b/src/packs/items/weapons/weapon_Greatbow_MXBpbqQsZFln4rZk.json @@ -113,18 +113,7 @@ "name": "Powerful", "description": "On a successful attack, roll an additional damage die and discard the lowest result.", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "changes": [ - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" - } - ], + "changes": [], "_id": "K4VgrDjVj1U1m9Ie", "type": "base", "system": {}, diff --git a/src/packs/items/weapons/weapon_Greatstaff_Yk8pTEmyLLi4095S.json b/src/packs/items/weapons/weapon_Greatstaff_Yk8pTEmyLLi4095S.json index 0fbfc2b4..66c12e5e 100644 --- a/src/packs/items/weapons/weapon_Greatstaff_Yk8pTEmyLLi4095S.json +++ b/src/packs/items/weapons/weapon_Greatstaff_Yk8pTEmyLLi4095S.json @@ -113,18 +113,7 @@ "name": "Powerful", "description": "On a successful attack, roll an additional damage die and discard the lowest result.", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "changes": [ - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" - } - ], + "changes": [], "_id": "904orawScurM9GjG", "type": "base", "system": {}, diff --git a/src/packs/items/weapons/weapon_Greatsword_70ysaFJDREwTgvZa.json b/src/packs/items/weapons/weapon_Greatsword_70ysaFJDREwTgvZa.json index 4707e397..f60e438d 100644 --- a/src/packs/items/weapons/weapon_Greatsword_70ysaFJDREwTgvZa.json +++ b/src/packs/items/weapons/weapon_Greatsword_70ysaFJDREwTgvZa.json @@ -118,16 +118,6 @@ "key": "system.evasion", "mode": 2, "value": "-1" - }, - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" } ], "_id": "cffkpiwGpEGhjiUC", diff --git a/src/packs/items/weapons/weapon_Improved_Greatstaff_LCuTrYXi4lhg6LqW.json b/src/packs/items/weapons/weapon_Improved_Greatstaff_LCuTrYXi4lhg6LqW.json index 5faa0b0e..cf1bdf63 100644 --- a/src/packs/items/weapons/weapon_Improved_Greatstaff_LCuTrYXi4lhg6LqW.json +++ b/src/packs/items/weapons/weapon_Improved_Greatstaff_LCuTrYXi4lhg6LqW.json @@ -113,18 +113,7 @@ "name": "Powerful", "description": "On a successful attack, roll an additional damage die and discard the lowest result.", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "changes": [ - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" - } - ], + "changes": [], "_id": "hnayB09P25ZW3gVY", "type": "base", "system": {}, diff --git a/src/packs/items/weapons/weapon_Improved_Greatsword_FPX4ouDrxXiQ5MDf.json b/src/packs/items/weapons/weapon_Improved_Greatsword_FPX4ouDrxXiQ5MDf.json index f8407b13..f71e5ea6 100644 --- a/src/packs/items/weapons/weapon_Improved_Greatsword_FPX4ouDrxXiQ5MDf.json +++ b/src/packs/items/weapons/weapon_Improved_Greatsword_FPX4ouDrxXiQ5MDf.json @@ -118,16 +118,6 @@ "key": "system.evasion", "mode": 2, "value": "-1" - }, - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" } ], "_id": "2nl35v8sPAudiOIb", diff --git a/src/packs/items/weapons/weapon_Legendary_Greatstaff_jDtvEabkHY1GFgfc.json b/src/packs/items/weapons/weapon_Legendary_Greatstaff_jDtvEabkHY1GFgfc.json index 0d317f0d..a5ea82f9 100644 --- a/src/packs/items/weapons/weapon_Legendary_Greatstaff_jDtvEabkHY1GFgfc.json +++ b/src/packs/items/weapons/weapon_Legendary_Greatstaff_jDtvEabkHY1GFgfc.json @@ -113,18 +113,7 @@ "name": "Powerful", "description": "On a successful attack, roll an additional damage die and discard the lowest result.", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "changes": [ - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" - } - ], + "changes": [], "_id": "OV1Ly7vX4owBUgLQ", "type": "base", "system": {}, diff --git a/src/packs/items/weapons/weapon_Legendary_Greatsword_zMZ46F9VR7zdTxb9.json b/src/packs/items/weapons/weapon_Legendary_Greatsword_zMZ46F9VR7zdTxb9.json index fb7a2ed3..840e7ec7 100644 --- a/src/packs/items/weapons/weapon_Legendary_Greatsword_zMZ46F9VR7zdTxb9.json +++ b/src/packs/items/weapons/weapon_Legendary_Greatsword_zMZ46F9VR7zdTxb9.json @@ -118,16 +118,6 @@ "key": "system.evasion", "mode": 2, "value": "-1" - }, - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" } ], "_id": "oRCiXSElN5xufUfn", diff --git a/src/packs/items/weapons/weapon_Mage_Orb_XKBmBUEoGLdLcuqQ.json b/src/packs/items/weapons/weapon_Mage_Orb_XKBmBUEoGLdLcuqQ.json index 8d3fd741..3b5983f5 100644 --- a/src/packs/items/weapons/weapon_Mage_Orb_XKBmBUEoGLdLcuqQ.json +++ b/src/packs/items/weapons/weapon_Mage_Orb_XKBmBUEoGLdLcuqQ.json @@ -113,18 +113,7 @@ "name": "Powerful", "description": "On a successful attack, roll an additional damage die and discard the lowest result.", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", - "changes": [ - { - "key": "system.bonuses.damage.primaryWeapon.extraDice", - "mode": 2, - "value": "1" - }, - { - "key": "system.rules.weapon.dropLowestDamageDice", - "mode": 5, - "value": "1" - } - ], + "changes": [], "_id": "2J6vzNUel78JFypp", "type": "base", "system": {}, From 78c6f3bf48f4ffc0ad1b5c7b184c64960817058c Mon Sep 17 00:00:00 2001 From: WBHarry Date: Fri, 16 Jan 2026 09:39:22 +0100 Subject: [PATCH 18/48] Removed a 'aware aware' duplicate typo --- module/applications/sheets/actors/character.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 806d7a45..e11fee05 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -712,7 +712,7 @@ export default class CharacterSheet extends DHBaseActorSheet { headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { ability: abilityLabel }), - effects: await await game.system.api.data.actions.actionsTypes.base.getEffects(this.document), + effects: await game.system.api.data.actions.actionsTypes.base.getEffects(this.document), roll: { trait: button.dataset.attribute, type: 'trait' From d626285a88a6054e8842f8492d5d47a4f5fd69b6 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Fri, 16 Jan 2026 10:05:38 +0100 Subject: [PATCH 19/48] . (#1545) --- module/documents/chatMessage.mjs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index c965f2e5..2f23cc1a 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -194,7 +194,16 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm')); this.consumeOnSuccess(); - this.system.action?.workflow.get('applyDamage')?.execute(config, targets, true); + if (this.system.action) this.system.action.workflow.get('applyDamage')?.execute(config, targets, true); + else { + for (const target of targets) { + const actor = await foundry.utils.fromUuid(target.actorId); + if (!actor) continue; + + if (this.system.hasHealing) actor.takeHealing(this.system.damage); + else actor.takeDamage(this.system.damage); + } + } } async onRollSave(event) { From 325a48532b8f22c5322a62a50bc64056bc66852e Mon Sep 17 00:00:00 2001 From: WBHarry Date: Fri, 16 Jan 2026 10:16:01 +0100 Subject: [PATCH 20/48] Nullable fix in baseAction.getEffects --- module/data/action/baseAction.mjs | 4 ++-- system.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index 0addc6b3..b5f95aff 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -274,13 +274,13 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel */ static async getEffects(actor, effectParent) { if (!actor) return []; - + return Array.from(await actor.allApplicableEffects()).filter(effect => { /* Effects on weapons only ever apply for the weapon itself */ if (effect.parent.type === 'weapon') { /* Unless they're secondary - then they apply only to other primary weapons */ if (effect.parent.system.secondary) { - if (effectParent.type !== 'weapon' || effectParent.system.secondary) return false; + if (effectParent?.type !== 'weapon' || effectParent?.system.secondary) return false; } else if (effectParent?.id !== effect.parent.id) return false; } diff --git a/system.json b/system.json index 50b4cd2d..50a9c83b 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.5.1", + "version": "1.5.2", "compatibility": { "minimum": "13.346", "verified": "13.351", From 98cf6fa6de1d2bb61f0a9b9c4a4d2df385120b70 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:13:26 +0100 Subject: [PATCH 21/48] [Feature] Character Creation Confirmations (#1533) * Added confirmation on ignoring character setup. Added reset option to character sheet. * Removed the system setting for playerCanEdit. It's always available now. --- lang/en.json | 11 ++-- .../applications/sheets/actors/character.mjs | 51 ++++++++++++++++--- module/data/settings/Automation.mjs | 5 -- .../settings/automation-settings/general.hbs | 1 - templates/sheets/actors/character/header.hbs | 6 +-- 5 files changed, 52 insertions(+), 22 deletions(-) diff --git a/lang/en.json b/lang/en.json index 8e64ab7d..870e1b2b 100755 --- a/lang/en.json +++ b/lang/en.json @@ -237,10 +237,13 @@ "confirmText": "Would you like to level up your companion {name} by {levelChange} levels at this time? (You can do it manually later)" }, "viewLevelups": "View Levelups", + "resetCharacter": "Reset Character", "viewParty": "View Party", "InvalidOldCharacterImportTitle": "Old Character Import", "InvalidOldCharacterImportText": "Character data exported prior to system version 1.1 will not generate a complete character. Do you wish to continue?", - "cancelBeastform": "Cancel Beastform" + "cancelBeastform": "Cancel Beastform", + "resetCharacterConfirmationTitle": "Reset Character", + "resetCharacterConfirmationContent": "You are reseting all character data except name and portrait. Are you sure?" }, "Companion": { "FIELDS": { @@ -314,6 +317,8 @@ "selectPrimaryWeapon": "Select Primary Weapon", "selectSecondaryWeapon": "Select Secondary Weapon", "selectSubclass": "Select Subclass", + "setupSkipTitle": "Skipping Character Setup", + "setupSkipContent": "You are skipping the Character Setup by adding this manually. The character setup is the blinking arrows in the top-right. Are you sure you want to continue?", "startingItems": "Starting Items", "story": "Story", "storyExplanation": "Select which background and connection prompts you want to copy into your character's background.", @@ -2450,10 +2455,6 @@ "label": "Show Resource Change Scrolltexts", "hint": "When a character is damaged, uses armor etc, a scrolling text will briefly appear by the token to signify this." }, - "playerCanEditSheet": { - "label": "Players Can Manually Edit Character Settings", - "hint": "Players are allowed to access the manual Character Settings and change their statistics beyond the rules." - }, "roll": { "roll": { "label": "Roll", diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index e11fee05..d691c129 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -27,6 +27,7 @@ export default class CharacterSheet extends DHBaseActorSheet { makeDeathMove: CharacterSheet.#makeDeathMove, levelManagement: CharacterSheet.#levelManagement, viewLevelups: CharacterSheet.#viewLevelups, + resetCharacter: CharacterSheet.#resetCharacter, toggleEquipItem: CharacterSheet.#toggleEquipItem, toggleResourceDice: CharacterSheet.#toggleResourceDice, handleResourceDice: CharacterSheet.#handleResourceDice, @@ -42,6 +43,11 @@ export default class CharacterSheet extends DHBaseActorSheet { icon: 'fa-solid fa-angles-up', label: 'DAGGERHEART.ACTORS.Character.viewLevelups', action: 'viewLevelups' + }, + { + icon: 'fa-solid fa-arrow-rotate-left', + label: 'DAGGERHEART.ACTORS.Character.resetCharacter', + action: 'resetCharacter' } ] }, @@ -220,13 +226,6 @@ export default class CharacterSheet extends DHBaseActorSheet { async _preparePartContext(partId, context, options) { context = await super._preparePartContext(partId, context, options); switch (partId) { - case 'header': - const { playerCanEditSheet, levelupAuto } = game.settings.get( - CONFIG.DH.id, - CONFIG.DH.SETTINGS.gameSettings.Automation - ); - context.showSettings = game.user.isGM || !levelupAuto || (levelupAuto && playerCanEditSheet); - break; case 'loadout': await this._prepareLoadoutContext(context, options); break; @@ -666,6 +665,32 @@ export default class CharacterSheet extends DHBaseActorSheet { new LevelupViewMode(this.document).render({ force: true }); } + /** + * 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) + ); + } + /** * Opens the Death Move interface for the character. * @type {ApplicationClickAction} @@ -956,6 +981,18 @@ export default class CharacterSheet extends DHBaseActorSheet { } async _onDropItem(event, item) { + const setupCriticalItemTypes = ['class', 'subclass', 'ancestry', 'community']; + if (this.document.system.needsCharacterSetup && setupCriticalItemTypes.includes(item.type)) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.localize('DAGGERHEART.APPLICATIONS.CharacterCreation.setupSkipTitle') + }, + content: game.i18n.localize('DAGGERHEART.APPLICATIONS.CharacterCreation.setupSkipContent') + }); + + if (!confirmed) return; + } + if (this.document.uuid === item.parent?.uuid) { return super._onDropItem(event, item); } diff --git a/module/data/settings/Automation.mjs b/module/data/settings/Automation.mjs index 3376b153..bff0bae9 100644 --- a/module/data/settings/Automation.mjs +++ b/module/data/settings/Automation.mjs @@ -55,11 +55,6 @@ export default class DhAutomation extends foundry.abstract.DataModel { initial: true, label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.resourceScrollTexts.label' }), - playerCanEditSheet: new fields.BooleanField({ - required: true, - initial: false, - label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.playerCanEditSheet.label' - }), defeated: new fields.SchemaField({ enabled: new fields.BooleanField({ required: true, diff --git a/templates/settings/automation-settings/general.hbs b/templates/settings/automation-settings/general.hbs index d49ef9b8..bd91b2b1 100644 --- a/templates/settings/automation-settings/general.hbs +++ b/templates/settings/automation-settings/general.hbs @@ -18,7 +18,6 @@ {{formGroup settingFields.schema.fields.hordeDamage value=settingFields._source.hordeDamage localize=true}} {{formGroup settingFields.schema.fields.effects.fields.rangeDependent value=settingFields._source.effects.rangeDependent localize=true}} {{formGroup settingFields.schema.fields.levelupAuto value=settingFields._source.levelupAuto localize=true}} - {{formGroup settingFields.schema.fields.playerCanEditSheet value=settingFields._source.playerCanEditSheet localize=true}} {{formGroup settingFields.schema.fields.damageReductionRulesDefault value=settingFields._source.damageReductionRulesDefault localize=true}} {{formGroup settingFields.schema.fields.resourceScrollTexts value=settingFields._source.resourceScrollTexts localize=true}} diff --git a/templates/sheets/actors/character/header.hbs b/templates/sheets/actors/character/header.hbs index 1459e10b..87319dbb 100644 --- a/templates/sheets/actors/character/header.hbs +++ b/templates/sheets/actors/character/header.hbs @@ -123,9 +123,7 @@ {{/each}} - {{#> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' showSettings=showSettings }} - {{#if ../showSettings}} - - {{/if}} + {{#> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }} + {{/'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}} \ No newline at end of file From 22383613f15bf1423080a0ee32432664f591c95e Mon Sep 17 00:00:00 2001 From: Chris Ryan <73275196+chrisryan10@users.noreply.github.com> Date: Sat, 17 Jan 2026 01:43:29 +1000 Subject: [PATCH 22/48] Ensure effect is Applied to Actor (#1547) Co-authored-by: Chris Ryan --- .../domains/domainCard_Bold_Presence_tdsL00yTSLNgZWs6.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packs/domains/domainCard_Bold_Presence_tdsL00yTSLNgZWs6.json b/src/packs/domains/domainCard_Bold_Presence_tdsL00yTSLNgZWs6.json index 02698989..08110cca 100644 --- a/src/packs/domains/domainCard_Bold_Presence_tdsL00yTSLNgZWs6.json +++ b/src/packs/domains/domainCard_Bold_Presence_tdsL00yTSLNgZWs6.json @@ -81,7 +81,7 @@ "name": "Bold Presence", "img": "icons/magic/holy/barrier-shield-winged-blue.webp", "origin": "Compendium.daggerheart.domains.Item.tdsL00yTSLNgZWs6", - "transfer": false, + "transfer": true, "_id": "2XEYhuAcRGTtqvED", "type": "base", "system": { From 07bdd48199ddf64678f647c4a72b232418eea712 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Fri, 16 Jan 2026 21:37:04 +0100 Subject: [PATCH 23/48] [Fix] 1548 - Standalone Item Add Actions (#1549) * Fixed so that items not on an actor don't error out on creating actions * Fixed deletion of items error --- module/data/item/base.mjs | 2 +- module/data/registeredTriggers.mjs | 2 +- module/documents/item.mjs | 2 +- system.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 0c9fdabe..2399b7db 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -240,7 +240,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { game.system.registeredTriggers.unregisterTriggers(triggersToRemove, this.parent.uuid); - if (!(this.parent.parent.token instanceof game.system.api.documents.DhToken)) { + if (this.parent.parent && !(this.parent.parent.token instanceof game.system.api.documents.DhToken)) { for (const token of this.parent.parent.getActiveTokens()) { game.system.registeredTriggers.unregisterTriggers( triggersToRemove, diff --git a/module/data/registeredTriggers.mjs b/module/data/registeredTriggers.mjs index fe962c5e..8a100585 100644 --- a/module/data/registeredTriggers.mjs +++ b/module/data/registeredTriggers.mjs @@ -60,7 +60,7 @@ export default class RegisteredTriggers extends Map { unregisterItemTriggers(items) { for (const item of items) { - if (!item.system.actions.size) continue; + if (!item.system.actions?.size) continue; const triggers = (item.system.actions ?? []).reduce((acc, action) => { acc.push(...action.triggers.map(x => x.trigger)); diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 0a163dab..fe62c5bd 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -217,7 +217,7 @@ export default class DHItem extends foundry.documents.Item { game.system.registeredTriggers.unregisterTriggers(triggerKeys, this.uuid); - if (!(this.actor.parent instanceof game.system.api.documents.DhToken)) { + if (this.actor && !(this.actor.parent instanceof game.system.api.documents.DhToken)) { for (const token of this.actor.getActiveTokens()) { game.system.registeredTriggers.unregisterTriggers(triggerKeys, `${token.document.uuid}.${this.uuid}`); } diff --git a/system.json b/system.json index 50a9c83b..8b6081a4 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.5.2", + "version": "1.5.3", "compatibility": { "minimum": "13.346", "verified": "13.351", From 4ce8fbb84c7f79892aa1c8fc39de40aa537f5590 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Sat, 17 Jan 2026 01:42:40 +0100 Subject: [PATCH 24/48] Raised version --- system.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system.json b/system.json index 8b6081a4..2c68f785 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.5.3", + "version": "1.5.4", "compatibility": { "minimum": "13.346", "verified": "13.351", From 9d75157e1711fa921fef0c2e2dcf8fcc0e39380c Mon Sep 17 00:00:00 2001 From: Chris Ryan <73275196+chrisryan10@users.noreply.github.com> Date: Sun, 18 Jan 2026 00:11:50 +1000 Subject: [PATCH 25/48] [Feature] Death moves and Fate rolls (#1463) * Update the death move descriptions * Renamed to DhDeathMove * Partial Fate Roll creation and Fate Roll Enricher (/fr) * Hide stuff not required for fate roll * Hide formula display; code removal; start to add Fear die as a choice for Fate roll * Fix chat message display; start moving towards supporting Hope and Fear for Fate roll * /fr now supports type=X, where X is Hope or Fear, if not supplied, defaults to Hope * Fixed DSN rolling; removed console messages; chat message clean up * Add localisation entry * Trying to sort out the button for the fate roll * Style the fate message based on Hope/Fear colors. * Partial improvement on the fate template buttons - chat display is correct, but the roll dialog is wrong * Fixed enricher button; localization fixes; debug cleanup * Error checking for the fate type parsing in all potential problem locations * Added localization for the fate type parsing error * Start on Avoid Death death move * debug stuff * More death moves setup/testing * Avoid fate scars update in place, with scars migrating to an integer value. * Remove some debug code; add Blaze Of Glory shell * Start on Guaranteed Critical for Blaze of Glory * Partial implementation of Blaze of Glory * Dice/critical checks/tests * Moved detection of guaranteed critical to before the roll dialog is created, so it can be skipped; removed debug code * Remove debug * Update Blaze of Glory effect description * Risk It All - critical roll - clear all stress and HP * Auto remove all marked stress and HP for Risk It All, if Hope value rolled covers it. * Display the Death Move description in chat expanded if the appropriate config setting is on * Made the Blaze of Glory ActiveEffect image use configured version * Update the current Hope value if the scar value change affects it * Scars management in the Character details editor * Separate less file for the Death Moves instead of reusing Downtime * Added result messages to the Death Move chat output and removed debug statements * Some localization, style and smaller changes * Fixed RiskItAll resource handling method * Risk It All success chat message start * [Add] Hope/Scar Interplay (#1531) * Migrated character.maxHope to homebrew settings * Added a visual for scars * . * . * Pass the hope value in the button data; skeleton risk it all dialog to fill out. * Start on risk it dialog * More dialog stuff * Remove non-existent field * Dialog templating and logic * . * Ensure effect is Applied to Actor (#1547) Co-authored-by: Chris Ryan * [Fix] 1548 - Standalone Item Add Actions (#1549) * Fixed so that items not on an actor don't error out on creating actions * Fixed deletion of items error * Raised version * Fix the sliders to do the correct maximums * Pass the actor id through the button; fix /dr and /fr flavor text * Remove debug message --------- Co-authored-by: Chris Ryan Co-authored-by: WBHarry Co-authored-by: WBHarry <89362246+WBHarry@users.noreply.github.com> --- daggerheart.mjs | 40 +++- lang/en.json | 35 +++- module/applications/dialogs/_module.mjs | 1 + module/applications/dialogs/d20RollDialog.mjs | 2 +- module/applications/dialogs/deathMove.mjs | 148 +++++++++++++- .../applications/dialogs/riskItAllDialog.mjs | 94 +++++++++ .../applications/sheets/actors/character.mjs | 9 +- module/applications/ui/chatLog.mjs | 17 +- module/data/action/baseAction.mjs | 6 +- module/data/actor/character.mjs | 44 ++++- module/data/chat-message/_modules.mjs | 1 + module/data/settings/Homebrew.mjs | 7 + module/dice/_module.mjs | 1 + module/dice/dualityRoll.mjs | 47 +++-- module/dice/fateRoll.mjs | 85 +++++++++ module/documents/actor.mjs | 14 +- module/documents/chatMessage.mjs | 9 + module/enrichers/DualityRollEnricher.mjs | 8 +- module/enrichers/FateRollEnricher.mjs | 80 ++++++++ module/enrichers/_module.mjs | 11 +- module/helpers/utils.mjs | 22 ++- .../death-move/death-move-container.less | 111 +++++------ styles/less/dialog/index.less | 4 +- styles/less/dialog/risk-it-all/sheet.less | 60 ++++++ styles/less/global/enrichment.less | 1 + .../less/sheets/actors/character/header.less | 5 + styles/less/ui/chat/chat.less | 26 ++- styles/less/ui/chat/deathmoves.less | 152 +++++++++++++++ styles/less/ui/index.less | 1 + system.json | 1 + templates/dialogs/dice-roll/rollSelection.hbs | 180 +++++++++++------- templates/dialogs/riskItAllDialog.hbs | 39 ++++ .../settings/homebrew-settings/settings.hbs | 1 + .../action-settings/trigger.hbs | 2 +- .../character-settings/details.hbs | 2 +- templates/sheets/actors/character/header.hbs | 5 + templates/ui/chat/deathMove.hbs | 27 ++- templates/ui/chat/parts/roll-part.hbs | 126 +++++++----- 38 files changed, 1166 insertions(+), 258 deletions(-) create mode 100644 module/applications/dialogs/riskItAllDialog.mjs create mode 100644 module/dice/fateRoll.mjs create mode 100644 module/enrichers/FateRollEnricher.mjs create mode 100644 styles/less/dialog/risk-it-all/sheet.less create mode 100644 styles/less/ui/chat/deathmoves.less create mode 100644 templates/dialogs/riskItAllDialog.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index 3abcd210..23f153dd 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -8,8 +8,9 @@ import * as fields from './module/data/fields/_module.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs'; import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs'; -import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll } from './module/dice/_module.mjs'; +import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll } from './module/dice/_module.mjs'; import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs'; +import { enrichedFateRoll, getFateTypeData } from './module/enrichers/FateRollEnricher.mjs'; import { handlebarsRegistration, runMigrations, @@ -24,12 +25,13 @@ import TokenManager from './module/documents/tokenManager.mjs'; CONFIG.DH = SYSTEM; CONFIG.TextEditor.enrichers.push(...enricherConfig); -CONFIG.Dice.rolls = [BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll]; +CONFIG.Dice.rolls = [BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll]; CONFIG.Dice.daggerheart = { DHRoll: DHRoll, DualityRoll: DualityRoll, D20Roll: D20Roll, - DamageRoll: DamageRoll + DamageRoll: DamageRoll, + FateRoll: FateRoll }; CONFIG.Actor.documentClass = documents.DhpActor; @@ -298,8 +300,8 @@ Hooks.on('chatMessage', (_, message) => { const difficulty = rollCommand.difficulty; const target = getCommandTarget({ allowNull: true }); - const title = traitValue - ? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { + const title = flavor ?? + traitValue ? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label) }) : game.i18n.localize('DAGGERHEART.GENERAL.duality'); @@ -316,6 +318,34 @@ Hooks.on('chatMessage', (_, message) => { }); return false; } + + if (message.startsWith('/fr')) { + const result = + message.trim().toLowerCase() === '/fr' ? { result: {} } : rollCommandToJSON(message.replace(/\/fr\s?/, '')); + + if (!result) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateParsing')); + return false; + } + + const { result: rollCommand, flavor } = result; + const fateTypeData = getFateTypeData(rollCommand?.type); + + if (!fateTypeData) + return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing')); + + const { value: fateType, label: fateTypeLabel } = fateTypeData; + const target = getCommandTarget({ allowNull: true }); + const title = flavor ?? game.i18n.localize('DAGGERHEART.GENERAL.fateRoll'); + + enrichedFateRoll({ + target, + title, + label: fateTypeLabel, + fateType + }); + return false; + } }); const updateActorsRangeDependentEffects = async token => { diff --git a/lang/en.json b/lang/en.json index 870e1b2b..69965b9e 100755 --- a/lang/en.json +++ b/lang/en.json @@ -619,6 +619,13 @@ "title": "{name} Resource", "rerollDice": "Reroll Dice" }, + "RiskItAllDialog": { + "title": "{name} - Risk It All", + "subtitle": "Clear Stress and Hit Points", + "remainingTitle": "Remaining Points", + "clearResource": "Clear {resource}", + "finalTitle": "Final Character Resources" + }, "TagTeamSelect": { "title": "Tag Team Roll", "leaderTitle": "Initiating Character", @@ -1017,15 +1024,15 @@ "DeathMoves": { "avoidDeath": { "name": "Avoid Death", - "description": "You drop unconscious temporarily and work with the GM to describe how the situation gets much worse because of it. Then roll your Fear die; if its value is equal to or under your Level, take a Scar." + "description": "Your character avoids death and faces the consequences. They temporarily drop unconscious, and then you work with the GM to describe how the situation worsens. While unconscious, your character can't move or act, and they can't be targeted by an attack. They return to consciousness when an ally clears 1 or more of their marked Hit Points or when the party finishes a long rest. After your character falls unconscious, roll your Hope Die. If its value is equal to or less than your character's level, they gain a scar: permanently cross out a Hope slot and work with the GM to determine its lasting narrative impact and how, if possible, it can be restored. If you ever cross out your last Hope slot, your character's journey ends." }, "riskItAll": { "name": "Risk It All", - "description": "Roll your Duality Dice. If Hope is higher, you stay on your feet and clear an amount of Hit Points and/or Stress equal to the value of the Hope die (divide the Hope die value up between these however you’d prefer). If your Fear die is higher, you cross through the veil of death. If the Duality Dice are tied, you stay on your feet and clear all Hit Points and Stress." + "description": "Roll your Duality Dice. If the Hope Die is higher, your character stays on their feet and clears a number of Hit Points or Stress equal to the value of the Hope Die (you can divide the Hope Die value between Hit Points and Stress however you'd prefer). If the Fear Die is higher, your character crosses through the veil of death. If the Duality Dice show matching results, your character stays up and clears all Hit Points and Stress." }, "blazeOfGlory": { "name": "Blaze Of Glory", - "description": "With Blaze of Glory, the player is accepting death for the character. Take one action (at GM discretion), which becomes an automatic critical success, then cross through the veil of death." + "description": "Your character embraces death and goes out in a blaze of glory. Take one final action. It automatically critically succeeds (with GM approval), and then you cross through the veil of death. NOTE: A Blaze of Glory effect has been added to your character. Any Duality Roll will automatically be a critical." } }, "DomainCardTypes": { @@ -2066,6 +2073,7 @@ "description": "Description", "main": "Data", "information": "Information", + "itemFeatures": "Item Features", "notes": "Notes", "inventory": "Inventory", "loadout": "Loadout", @@ -2141,6 +2149,7 @@ "dropActorsHere": "Drop Actors here", "dropFeaturesHere": "Drop Features here", "duality": "Duality", + "dualityDice": "Duality Dice", "dualityRoll": "Duality Roll", "enabled": "Enabled", "evasion": "Evasion", @@ -2150,11 +2159,14 @@ "plural": "Experiences" }, "failure": "Failure", + "fate": "Fate", + "fateRoll": "Fate Roll", "fear": "Fear", "features": "Features", "formula": "Formula", "general": "General", "gm": "GM", + "guaranteedCriticalSuccess": "Guaranteed Critical Success", "healing": "Healing", "healingRoll": "Healing Roll", "hit": { @@ -2214,6 +2226,7 @@ "rollWith": "{roll} Roll", "save": "Save", "scalable": "Scalable", + "scars": "Scars", "situationalBonus": "Situational Bonus", "spent": "Spent", "step": "Step", @@ -2510,6 +2523,7 @@ "resetMovesText": "Are you sure you want to reset?", "FIELDS": { "maxFear": { "label": "Max Fear" }, + "maxHope": { "label": "Max Hope" }, "traitArray": { "label": "Initial Trait Modifiers" }, "maxLoadout": { "label": "Max Cards in Loadout", @@ -2657,7 +2671,16 @@ "currentTarget": "Current" }, "deathMove": { - "title": "Death Move" + "title": "Death Move", + "gainScar": "You gained a scar.", + "avoidScar": "You have avoided a new scar.", + "journeysEnd": "You have {scars} Scars and have crossed out your last Hope slot. Your character's journey ends.", + "riskItAllCritical": "Critical Rolled, clearing all marked Stress and Hit Points.", + "riskItAllFailure": "The fear die rolled higher. You have crossed through the veil of death.", + "blazeOfGlory": "Blaze of Glory Effect Added!", + "riskItAllDialogButton": "Clear Stress And Hit Points.", + "riskItAllSuccessWithEnoughHope": "The Hope value is more than the marked Stress and Hit Points. Both are cleared fully.", + "riskItAllSuccess": "The hope die rolled higher, clear up to {hope} Stress And Hit Points." }, "dicePool": { "title": "Dice Pool" @@ -2779,7 +2802,9 @@ "noAssignedPlayerCharacter": "You have no assigned character.", "noSelectedToken": "You have no selected token", "onlyUseableByPC": "This can only be used with a PC token", - "dualityParsing": "Duality roll not properly formated", + "dualityParsing": "Duality roll not properly formatted", + "fateParsing": "Fate roll not properly formatted", + "fateTypeParsing": "Fate roll not properly formatted, bad fate type. Valid types are 'Hope' and 'Fear'", "attributeFaulty": "The supplied Attribute doesn't exist", "domainCardWrongDomain": "You don't have access to that Domain", "domainCardToHighLevel": "The Domain Card is too high level to be selected", diff --git a/module/applications/dialogs/_module.mjs b/module/applications/dialogs/_module.mjs index 92038c41..d43045e6 100644 --- a/module/applications/dialogs/_module.mjs +++ b/module/applications/dialogs/_module.mjs @@ -14,3 +14,4 @@ export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs'; export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs'; export { default as GroupRollDialog } from './group-roll-dialog.mjs'; export { default as TagTeamDialog } from './tagTeamDialog.mjs'; +export { default as RiskItAllDialog } from './riskItAllDialog.mjs'; diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index 441842dc..6f320152 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -123,7 +123,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio context.formula = this.roll.constructFormula(this.config); if (this.actor?.system?.traits) context.abilities = this.getTraitModifiers(); - context.showReaction = !this.config.roll?.type || context.rollType === 'DualityRoll'; + context.showReaction = !this.config.skips?.reaction && context.rollType === 'DualityRoll'; context.reactionOverride = this.reactionOverride; } diff --git a/module/applications/dialogs/deathMove.mjs b/module/applications/dialogs/deathMove.mjs index d0686d2b..01df6057 100644 --- a/module/applications/dialogs/deathMove.mjs +++ b/module/applications/dialogs/deathMove.mjs @@ -1,11 +1,16 @@ -const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; +import { enrichedFateRoll } from '../../enrichers/FateRollEnricher.mjs'; +import { enrichedDualityRoll } from '../../enrichers/DualityRollEnricher.mjs'; -export default class DhpDeathMove extends HandlebarsApplicationMixin(ApplicationV2) { +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; +export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV2) { constructor(actor) { super({}); this.actor = actor; this.selectedMove = null; + this.showRiskItAllButton = false; + this.riskItAllButtonLabel = ''; + this.riskItAllHope = 0; } get title() { @@ -38,6 +43,107 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application return context; } + async handleAvoidDeath() { + const target = this.actor.uuid; + const config = await enrichedFateRoll({ + target, + title: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.avoidDeath.name'), + label: `${game.i18n.localize('DAGGERHEART.GENERAL.hope')} ${game.i18n.localize('DAGGERHEART.GENERAL.fateRoll')}`, + fateType: 'Hope' + }); + + if (!config.roll.fate) return; + + 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 + } + }); + + if (newScarAmount >= this.actor.system.resources.hope.max) { + return game.i18n.format('DAGGERHEART.UI.Chat.deathMove.journeysEnd', { scars: newScarAmount }); + } + + return game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.gainScar'); + } + + return game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.avoidScar'); + } + + async handleRiskItAll() { + const config = await enrichedDualityRoll({ + reaction: true, + traitValue: null, + target: this.actor, + difficulty: null, + title: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.riskItAll.name'), + label: game.i18n.localize('DAGGERHEART.GENERAL.dualityDice'), + actionType: null, + advantage: null, + customConfig: { skips: { resources: true, reaction: true } } + }); + + if (!config.roll.result) return; + + const clearAllStressAndHitpointsUpdates = [ + { key: 'hitPoints', clear: true }, + { key: 'stress', clear: true } + ]; + + let chatMessage = ''; + if (config.roll.isCritical) { + config.resourceUpdates.addResources(clearAllStressAndHitpointsUpdates); + chatMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.riskItAllCritical'); + } + + if (config.roll.result.duality == 1) { + if ( + config.roll.hope.value >= + this.actor.system.resources.hitPoints.value + this.actor.system.resources.stress.value + ) { + config.resourceUpdates.addResources(clearAllStressAndHitpointsUpdates); + chatMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.riskItAllSuccessWithEnoughHope'); + } else { + chatMessage = game.i18n.format('DAGGERHEART.UI.Chat.deathMove.riskItAllSuccess', { + hope: config.roll.hope.value + }); + this.showRiskItAllButton = true; + this.riskItAllHope = config.roll.hope.value; + this.riskItAllButtonLabel = game.i18n.format('DAGGERHEART.UI.Chat.deathMove.riskItAllDialogButton'); + } + } + + if (config.roll.result.duality == -1) { + chatMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.riskItAllFailure'); + } + + await config.resourceUpdates.updateResources(); + return chatMessage; + } + + async handleBlazeOfGlory() { + this.actor.createEmbeddedDocuments('ActiveEffect', [ + { + name: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.name'), + description: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.description'), + img: CONFIG.DH.GENERAL.deathMoves.blazeOfGlory.img, + changes: [ + { + key: 'system.rules.roll.guaranteedCritical', + mode: 2, + value: 'true' + } + ] + } + ]); + + return game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.blazeOfGlory'); + } + static selectMove(_, button) { const move = button.dataset.move; this.selectedMove = CONFIG.DH.GENERAL.deathMoves[move]; @@ -46,23 +152,49 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application } static async takeMove() { + this.close(); + + let result = ''; + + if (CONFIG.DH.GENERAL.deathMoves.blazeOfGlory === this.selectedMove) { + result = await this.handleBlazeOfGlory(); + } + + if (CONFIG.DH.GENERAL.deathMoves.avoidDeath === this.selectedMove) { + result = await this.handleAvoidDeath(); + } + + if (CONFIG.DH.GENERAL.deathMoves.riskItAll === this.selectedMove) { + result = await this.handleRiskItAll(); + } + + if (!result) return; + + const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance) + .expandRollMessage?.desc; const cls = getDocumentClass('ChatMessage'); + const msg = { user: game.user.id, content: await foundry.applications.handlebars.renderTemplate( 'systems/daggerheart/templates/ui/chat/deathMove.hbs', { player: this.actor.name, - actor: { name: this.actor.name, img: this.actor.img }, + actor: this.actor, + actorId: this.actor._id, author: game.users.get(game.user.id), title: game.i18n.localize(this.selectedMove.name), img: this.selectedMove.img, - description: game.i18n.localize(this.selectedMove.description) + description: game.i18n.localize(this.selectedMove.description), + result: result, + open: autoExpandDescription ? 'open' : '', + chevron: autoExpandDescription ? 'fa-chevron-up' : 'fa-chevron-down', + showRiskItAllButton: this.showRiskItAllButton, + riskItAllButtonLabel: this.riskItAllButtonLabel, + riskItAllHope: this.riskItAllHope } ), - title: game.i18n.localize( - 'DAGGERHEART.UI.Chat.deathMove.title' - ), + title: game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.title'), speaker: cls.getSpeaker(), flags: { daggerheart: { @@ -72,7 +204,5 @@ export default class DhpDeathMove extends HandlebarsApplicationMixin(Application }; cls.create(msg); - - this.close(); } } diff --git a/module/applications/dialogs/riskItAllDialog.mjs b/module/applications/dialogs/riskItAllDialog.mjs new file mode 100644 index 00000000..10fa1bb4 --- /dev/null +++ b/module/applications/dialogs/riskItAllDialog.mjs @@ -0,0 +1,94 @@ +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; +export default class RiskItAllDialog extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(actor, resourceValue) { + super({}); + + this.actor = actor; + this.resourceValue = resourceValue; + this.choices = { + hitPoints: 0, + stress: 0 + }; + } + + get title() { + return game.i18n.format('DAGGERHEART.APPLICATIONS.RiskItAllDialog.title', { name: this.actor.name }); + } + + static DEFAULT_OPTIONS = { + classes: ['daggerheart', 'dh-style', 'dialog', 'views', 'risk-it-all'], + position: { width: 280, height: 'auto' }, + window: { icon: 'fa-solid fa-dice fa-xl' }, + actions: { + finish: RiskItAllDialog.#finish + } + }; + + static PARTS = { + application: { + id: 'risk-it-all', + template: 'systems/daggerheart/templates/dialogs/riskItAllDialog.hbs' + } + }; + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + for (const input of htmlElement.querySelectorAll('.resource-container input')) + input.addEventListener('change', this.updateChoice.bind(this)); + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.resourceValue = this.resourceValue; + context.maxHitPointsValue = Math.min(this.resourceValue, this.actor.system.resources.hitPoints.max); + context.maxStressValue = Math.min(this.resourceValue, this.actor.system.resources.stress.max); + context.remainingResource = this.resourceValue - this.choices.hitPoints - this.choices.stress; + context.unfinished = context.remainingResource !== 0; + + context.choices = this.choices; + context.final = { + hitPoints: { + value: this.actor.system.resources.hitPoints.value - this.choices.hitPoints, + max: this.actor.system.resources.hitPoints.max + }, + stress: { + value: this.actor.system.resources.stress.value - this.choices.stress, + max: this.actor.system.resources.stress.max + } + }; + + context; + + return context; + } + + updateChoice(event) { + let value = Number.parseInt(event.target.value); + const choiceKey = event.target.dataset.choice; + const actorValue = this.actor.system.resources[choiceKey].value; + const remaining = this.resourceValue - this.choices.hitPoints - this.choices.stress; + const changeAmount = value - this.choices[choiceKey]; + + /* If trying to increase beyond remaining resource points, just increase to max available */ + if (remaining - changeAmount < 0) value = this.choices[choiceKey] + remaining; + else if (actorValue - value < 0) value = actorValue; + + this.choices[choiceKey] = value; + this.render(); + } + + static async #finish() { + const resourceUpdate = Object.keys(this.choices).reduce((acc, resourceKey) => { + const value = this.actor.system.resources[resourceKey].value - this.choices[resourceKey]; + acc[resourceKey] = { value }; + return acc; + }, {}); + + await this.actor.update({ + 'system.resources': resourceUpdate + }); + + this.close(); + } +} diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index d691c129..5c6bac3a 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -1,5 +1,5 @@ import DHBaseActorSheet from '../api/base-actor.mjs'; -import DhpDeathMove from '../../dialogs/deathMove.mjs'; +import DhDeathMove from '../../dialogs/deathMove.mjs'; import { abilities } from '../../../config/actorConfig.mjs'; import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs'; import DhCharacterCreation from '../../characterCreation/characterCreation.mjs'; @@ -696,7 +696,7 @@ export default class CharacterSheet extends DHBaseActorSheet { * @type {ApplicationClickAction} */ static async #makeDeathMove() { - await new DhpDeathMove(this.document).render({ force: true }); + await new DhDeathMove(this.document).render({ force: true }); } /** @@ -753,9 +753,8 @@ 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/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index c4a313fa..2b489f58 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -81,6 +81,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo html.querySelectorAll('.group-roll-header-expand-section').forEach(element => element.addEventListener('click', this.groupRollExpandSection) ); + html.querySelectorAll('.risk-it-all-button').forEach(element => + element.addEventListener('click', event => this.riskItAllClearStressAndHitPoints(event, data)) + ); }; setupHooks() { @@ -94,15 +97,17 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo /** Ensure the chat theme inherits the interface theme */ _replaceHTML(result, content, options) { - const themedElement = result.log?.querySelector(".chat-log"); - themedElement?.classList.remove("themed", "theme-light", "theme-dark"); + const themedElement = result.log?.querySelector('.chat-log'); + themedElement?.classList.remove('themed', 'theme-light', 'theme-dark'); super._replaceHTML(result, content, options); } /** Remove chat log theme from notifications area */ async _onFirstRender(result, content) { await super._onFirstRender(result, content); - document.querySelector("#chat-notifications .chat-log")?.classList.remove("themed", "theme-light", "theme-dark") + document + .querySelector('#chat-notifications .chat-log') + ?.classList.remove('themed', 'theme-light', 'theme-dark'); } async onRollSimple(event, message) { @@ -383,4 +388,10 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo }); event.target.closest('.group-roll-section').querySelector('.group-roll-content').classList.toggle('closed'); } + + async riskItAllClearStressAndHitPoints(event, data) { + const resourceValue = event.target.dataset.resourceValue; + const actor = game.actors.get(event.target.dataset.actorId); + new game.system.api.applications.dialogs.RiskItAllDialog(actor, resourceValue).render({ force: true }); + } } diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index b5f95aff..4e699f79 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -376,14 +376,14 @@ export class ResourceUpdateMap extends Map { if (!resource.key) continue; const existing = this.get(resource.key); - if (existing) { + if (!existing || resource.clear) { + this.set(resource.key, resource); + } else if (!existing?.clear) { this.set(resource.key, { ...existing, value: existing.value + (resource.value ?? 0), total: existing.total + (resource.total ?? 0) }); - } else { - this.set(resource.key, resource); } } } diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index f6ab7e3a..a7f99ca8 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -35,7 +35,14 @@ export default class DhCharacter extends BaseDataActor { 'DAGGERHEART.ACTORS.Character.maxHPBonus' ), stress: resourceField(6, 0, 'DAGGERHEART.GENERAL.stress', true), - hope: resourceField(6, 2, 'DAGGERHEART.GENERAL.hope') + hope: new fields.SchemaField({ + value: new fields.NumberField({ + initial: 2, + min: 0, + integer: true, + label: 'DAGGERHEART.GENERAL.hope' + }) + }) }), traits: new fields.SchemaField({ agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'), @@ -78,12 +85,7 @@ export default class DhCharacter extends BaseDataActor { bags: new fields.NumberField({ initial: 0, integer: true }), chests: new fields.NumberField({ initial: 0, integer: true }) }), - scars: new fields.TypedObjectField( - new fields.SchemaField({ - name: new fields.StringField({}), - description: new fields.StringField() - }) - ), + scars: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.scars' }), biography: new fields.SchemaField({ background: new fields.HTMLField(), connections: new fields.HTMLField(), @@ -301,6 +303,9 @@ export default class DhCharacter extends BaseDataActor { runeWard: new fields.BooleanField({ initial: false }), burden: new fields.SchemaField({ ignore: new fields.BooleanField() + }), + roll: new fields.SchemaField({ + guaranteedCritical: new fields.BooleanField() }) }) }; @@ -642,7 +647,9 @@ export default class DhCharacter extends BaseDataActor { ? armor.system.baseThresholds.severe + this.levelData.level.current : this.levelData.level.current * 2 }; - this.resources.hope.max -= Object.keys(this.scars).length; + + const globalHopeMax = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxHope; + this.resources.hope.max = globalHopeMax - this.scars; this.resources.hitPoints.max += this.class.value?.system?.hitPoints ?? 0; } @@ -699,6 +706,20 @@ export default class DhCharacter extends BaseDataActor { changes.system.experiences[experience].core = true; } } + + /* Scars can alter the amount of current hope */ + if (changes.system?.scars) { + const diff = this.system.scars - changes.system.scars; + const newHopeMax = this.system.resources.hope.max + diff; + const newHopeValue = Math.min(newHopeMax, this.system.resources.hope.value); + if (newHopeValue != this.system.resources.hope.value) { + if (!changes.system.resources) changes.system.resources = { hope: { value: 0 } }; + changes.system.resources.hope = { + ...changes.system.resources.hope, + value: changes.system.resources.hope.value + newHopeValue + }; + } + } } async _preDelete() { @@ -714,4 +735,11 @@ export default class DhCharacter extends BaseDataActor { t => !!t ); } + + static migrateData(source) { + if (typeof source.scars === 'object') source.scars = 0; + if (source.resources?.hope?.max) source.scars = Math.max(6 - source.resources.hope.max, 0); + + return super.migrateData(source); + } } diff --git a/module/data/chat-message/_modules.mjs b/module/data/chat-message/_modules.mjs index ec095aac..c671de31 100644 --- a/module/data/chat-message/_modules.mjs +++ b/module/data/chat-message/_modules.mjs @@ -8,6 +8,7 @@ export const config = { adversaryRoll: DHActorRoll, damageRoll: DHActorRoll, dualityRoll: DHActorRoll, + fateRoll: DHActorRoll, groupRoll: DHGroupRoll, systemMessage: DHSystemMessage }; diff --git a/module/data/settings/Homebrew.mjs b/module/data/settings/Homebrew.mjs index 7572c9ea..0138713c 100644 --- a/module/data/settings/Homebrew.mjs +++ b/module/data/settings/Homebrew.mjs @@ -23,6 +23,13 @@ export default class DhHomebrew extends foundry.abstract.DataModel { initial: 12, label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxFear.label' }), + maxHope: new fields.NumberField({ + required: true, + integer: true, + min: 0, + initial: 6, + label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxHope.label' + }), maxLoadout: new fields.NumberField({ required: true, integer: true, diff --git a/module/dice/_module.mjs b/module/dice/_module.mjs index e6755a74..b9339d87 100644 --- a/module/dice/_module.mjs +++ b/module/dice/_module.mjs @@ -3,3 +3,4 @@ export { default as D20Roll } from './d20Roll.mjs'; export { default as DamageRoll } from './damageRoll.mjs'; export { default as DHRoll } from './dhRoll.mjs'; export { default as DualityRoll } from './dualityRoll.mjs'; +export { default as FateRoll } from './fateRoll.mjs'; diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index aaca7400..0edbe5ad 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -12,6 +12,7 @@ export default class DualityRoll extends D20Roll { constructor(formula, data = {}, options = {}) { super(formula, data, options); this.rallyChoices = this.setRallyChoices(); + this.guaranteedCritical = options.guaranteedCritical; } static messageType = 'dualityRoll'; @@ -25,29 +26,23 @@ export default class DualityRoll extends D20Roll { } get dHope() { - // if ( !(this.terms[0] instanceof foundry.dice.terms.Die) ) return; if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); return this.dice[0]; - // return this.#hopeDice; } set dHope(faces) { if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); - this.terms[0].faces = this.getFaces(faces); - // this.#hopeDice = `d${face}`; + this.dice[0].faces = this.getFaces(faces); } get dFear() { - // if ( !(this.terms[1] instanceof foundry.dice.terms.Die) ) return; if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice(); return this.dice[1]; - // return this.#fearDice; } set dFear(faces) { if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice(); this.dice[1].faces = this.getFaces(faces); - // this.#fearDice = `d${face}`; } get dAdvantage() { @@ -90,26 +85,29 @@ export default class DualityRoll extends D20Roll { } get isCritical() { + if (this.guaranteedCritical) return true; if (!this.dHope._evaluated || !this.dFear._evaluated) return; return this.dHope.total === this.dFear.total; } get withHope() { - if (!this._evaluated) return; + if (!this._evaluated || this.guaranteedCritical) return; return this.dHope.total > this.dFear.total; } get withFear() { - if (!this._evaluated) return; + if (!this._evaluated || this.guaranteedCritical) return; return this.dHope.total < this.dFear.total; } get totalLabel() { - const label = this.withHope - ? 'DAGGERHEART.GENERAL.hope' - : this.withFear - ? 'DAGGERHEART.GENERAL.fear' - : 'DAGGERHEART.GENERAL.criticalSuccess'; + const label = this.guaranteedCritical + ? 'DAGGERHEART.GENERAL.guaranteedCriticalSuccess' + : this.isCritical + ? 'DAGGERHEART.GENERAL.criticalSuccess' + : this.withHope + ? 'DAGGERHEART.GENERAL.hope' + : 'DAGGERHEART.GENERAL.fear'; return game.i18n.localize(label); } @@ -178,6 +176,21 @@ export default class DualityRoll extends D20Roll { return modifiers; } + static async buildConfigure(config = {}, message = {}) { + config.dialog ??= {}; + config.guaranteedCritical = config.data?.parent?.appliedEffects.reduce((a, c) => { + const change = c.changes.find(ch => ch.key === 'system.rules.roll.guaranteedCritical'); + if (change) a = true; + return a; + }, false); + + if (config.guaranteedCritical) { + config.dialog.configure = false; + } + + return super.buildConfigure(config, message); + } + getActionChangeKeys() { const changeKeys = new Set([`system.bonuses.roll.${this.options.actionType}`]); @@ -223,7 +236,7 @@ export default class DualityRoll extends D20Roll { data.hope = { dice: roll.dHope.denomination, - value: roll.dHope.total, + value: this.guaranteedCritical ? 0 : roll.dHope.total, rerolled: { any: roll.dHope.results.some(x => x.rerolled), rerolls: roll.dHope.results.filter(x => x.rerolled) @@ -231,7 +244,7 @@ export default class DualityRoll extends D20Roll { }; data.fear = { dice: roll.dFear.denomination, - value: roll.dFear.total, + value: this.guaranteedCritical ? 0 : roll.dFear.total, rerolled: { any: roll.dFear.results.some(x => x.rerolled), rerolls: roll.dFear.results.filter(x => x.rerolled) @@ -243,7 +256,7 @@ export default class DualityRoll extends D20Roll { }; data.result = { duality: roll.withHope ? 1 : roll.withFear ? -1 : 0, - total: roll.dHope.total + roll.dFear.total, + total: this.guaranteedCritical ? 0 : roll.dHope.total + roll.dFear.total, label: roll.totalLabel }; diff --git a/module/dice/fateRoll.mjs b/module/dice/fateRoll.mjs new file mode 100644 index 00000000..418c8465 --- /dev/null +++ b/module/dice/fateRoll.mjs @@ -0,0 +1,85 @@ +import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; +import D20Roll from './d20Roll.mjs'; +import { setDiceSoNiceForHopeFateRoll, setDiceSoNiceForFearFateRoll } from '../helpers/utils.mjs'; + +export default class FateRoll extends D20Roll { + constructor(formula, data = {}, options = {}) { + super(formula, data, options); + } + + static messageType = 'fateRoll'; + + static DefaultDialog = D20RollDialog; + + get title() { + return game.i18n.localize(`DAGGERHEART.GENERAL.fateRoll`); + } + + get dHope() { + if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); + return this.dice[0]; + } + + set dHope(faces) { + if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); + this.dice[0].faces = this.getFaces(faces); + } + + get dFear() { + if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); + return this.dice[0]; + } + + set dFear(faces) { + if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); + this.dice[0].faces = this.getFaces(faces); + } + + get isCritical() { + return false; + } + + get fateDie() { + return this.data.fateType; + } + + static getHooks(hooks) { + return [...(hooks ?? []), 'Fate']; + } + + /** @inheritDoc */ + static fromData(data) { + data.terms[0].class = foundry.dice.terms.Die.name; + return super.fromData(data); + } + + createBaseDice() { + if (this.dice[0] instanceof foundry.dice.terms.Die) { + this.terms = [this.terms[0]]; + return; + } + this.terms[0] = new foundry.dice.terms.Die({ faces: 12 }); + } + + static async buildEvaluate(roll, config = {}, message = {}) { + await super.buildEvaluate(roll, config, message); + + if (roll.fateDie === 'Hope') { + await setDiceSoNiceForHopeFateRoll(roll, config.roll.fate.dice); + } else { + await setDiceSoNiceForFearFateRoll(roll, config.roll.fate.dice); + } + } + + static postEvaluate(roll, config = {}) { + const data = super.postEvaluate(roll, config); + + data.fate = { + dice: roll.fateDie === 'Hope' ? roll.dHope.denomination : roll.dFear.denomination, + value: roll.fateDie === 'Hope' ? roll.dHope.total : roll.dFear.total, + fateDie: roll.fateDie + }; + + return data; + } +} diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 27c310ae..cec1a24d 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -764,16 +764,24 @@ export default class DhpActor extends Actor { }; } } else { + const valueFunc = (base, resource, baseMax) => { + if (resource.clear) return baseMax && base.inverted ? baseMax : 0; + + return (base.value ?? base) + resource.value; + }; switch (r.key) { case 'fear': ui.resources.updateFear( - game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) + r.value + valueFunc( + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), + r + ) ); break; case 'armor': if (this.system.armor?.system?.marks) { updates.armor.resources['system.marks.value'] = Math.max( - Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore), + Math.min(valueFunc(this.system.armor.system.marks, r), this.system.armorScore), 0 ); } @@ -782,7 +790,7 @@ export default class DhpActor extends Actor { if (this.system.resources?.[r.key]) { updates.actor.resources[`system.resources.${r.key}.value`] = Math.max( Math.min( - this.system.resources[r.key].value + r.value, + valueFunc(this.system.resources[r.key], r, this.system.resources[r.key].max), this.system.resources[r.key].max ), 0 diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index 2f23cc1a..e03c3cf0 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -87,6 +87,15 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { break; } } + if (this.type === 'fateRoll') { + html.classList.add('fate'); + if (this.system.roll?.fate.fateDie == 'Hope') { + html.classList.add('hope'); + } + if (this.system.roll?.fate.fateDie == 'Fear') { + html.classList.add('fear'); + } + } const autoExpandRoll = game.settings.get( CONFIG.DH.id, diff --git a/module/enrichers/DualityRollEnricher.mjs b/module/enrichers/DualityRollEnricher.mjs index 536847f7..f6f022f9 100644 --- a/module/enrichers/DualityRollEnricher.mjs +++ b/module/enrichers/DualityRollEnricher.mjs @@ -2,7 +2,7 @@ import { abilities } from '../config/actorConfig.mjs'; import { getCommandTarget, rollCommandToJSON } from '../helpers/utils.mjs'; export default function DhDualityRollEnricher(match, _options) { - const roll = rollCommandToJSON(match[1], match[0]); + const roll = rollCommandToJSON(match[0]); if (!roll) return match[0]; return getDualityMessage(roll.result, roll.flavor); @@ -80,7 +80,7 @@ export const renderDualityButton = async event => { }; export const enrichedDualityRoll = async ( - { reaction, traitValue, target, difficulty, title, label, advantage }, + { reaction, traitValue, target, difficulty, title, label, advantage, customConfig }, event ) => { const config = { @@ -94,7 +94,8 @@ export const enrichedDualityRoll = async ( type: reaction ? 'reaction' : null }, type: 'trait', - hasRoll: true + hasRoll: true, + ...(customConfig ?? {}) }; if (target) { @@ -105,4 +106,5 @@ export const enrichedDualityRoll = async ( config.source = { actor: null }; await CONFIG.Dice.daggerheart.DualityRoll.build(config); } + return config; }; diff --git a/module/enrichers/FateRollEnricher.mjs b/module/enrichers/FateRollEnricher.mjs new file mode 100644 index 00000000..c82bbcb2 --- /dev/null +++ b/module/enrichers/FateRollEnricher.mjs @@ -0,0 +1,80 @@ +import { getCommandTarget, rollCommandToJSON } from '../helpers/utils.mjs'; + +export default function DhFateRollEnricher(match, _options) { + const roll = rollCommandToJSON(match[0]); + if (!roll) return match[0]; + + return getFateMessage(roll.result, roll?.flavor); +} + +export function getFateTypeData(fateTypeValue) { + const value = fateTypeValue ? fateTypeValue.capitalize() : 'Hope'; + const lowercased = fateTypeValue?.toLowerCase?.() ?? 'hope'; + switch (lowercased) { + case 'hope': + case 'fear': + return { value, label: game.i18n.localize(`DAGGERHEART.GENERAL.${lowercased}`) }; + default: + return null; + } +} + +function getFateMessage(roll, flavor) { + const fateTypeData = getFateTypeData(roll?.type); + + if (!fateTypeData) + return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing')); + + const { value: fateType, label: fateTypeLabel } = fateTypeData; + const title = flavor ?? game.i18n.localize('DAGGERHEART.GENERAL.fateRoll'); + + const fateElement = document.createElement('span'); + fateElement.innerHTML = ` + + `; + + return fateElement; +} + +export const renderFateButton = async event => { + const button = event.currentTarget, + target = getCommandTarget({ allowNull: true }); + + const fateTypeData = getFateTypeData(button.dataset?.fatetype); + + if (!fateTypeData) ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing')); + const { value: fateType, label: fateTypeLabel } = fateTypeData; + + await enrichedFateRoll( + { + target, + title: button.dataset.title, + label: button.dataset.label, + fateType: fateType + }, + event + ); +}; + +export const enrichedFateRoll = async ({ target, title, label, fateType }, event) => { + const config = { + event: event ?? {}, + title: title, + headerTitle: label, + roll: {}, + hasRoll: true, + fateType: fateType, + skips: { reaction: true } + }; + + config.data = { experiences: {}, traits: {}, fateType: fateType }; + config.source = { actor: target?.uuid }; + await CONFIG.Dice.daggerheart.FateRoll.build(config); + return config; +}; diff --git a/module/enrichers/_module.mjs b/module/enrichers/_module.mjs index 0dcd870e..b80f166c 100644 --- a/module/enrichers/_module.mjs +++ b/module/enrichers/_module.mjs @@ -1,10 +1,11 @@ import { default as DhDamageEnricher, renderDamageButton } from './DamageEnricher.mjs'; import { default as DhDualityRollEnricher, renderDualityButton } from './DualityRollEnricher.mjs'; +import { default as DhFateRollEnricher, renderFateButton } from './FateRollEnricher.mjs'; import { default as DhEffectEnricher } from './EffectEnricher.mjs'; import { default as DhTemplateEnricher, renderMeasuredTemplate } from './TemplateEnricher.mjs'; import { default as DhLookupEnricher } from './LookupEnricher.mjs'; -export { DhDamageEnricher, DhDualityRollEnricher, DhEffectEnricher, DhTemplateEnricher }; +export { DhDamageEnricher, DhDualityRollEnricher, DhEffectEnricher, DhTemplateEnricher, DhFateRollEnricher }; export const enricherConfig = [ { @@ -15,6 +16,10 @@ export const enricherConfig = [ pattern: /\[\[\/dr\s?(.*?)\]\]({[^}]*})?/g, enricher: DhDualityRollEnricher }, + { + pattern: /\[\[\/fr\s?(.*?)\]\]({[^}]*})?/g, + enricher: DhFateRollEnricher + }, { pattern: /@Effect\[([^\[\]]*)\]({[^}]*})?/g, enricher: DhEffectEnricher @@ -38,6 +43,10 @@ export const enricherRenderSetup = element => { .querySelectorAll('.duality-roll-button') .forEach(element => element.addEventListener('click', renderDualityButton)); + element + .querySelectorAll('.fate-roll-button') + .forEach(element => element.addEventListener('click', renderFateButton)); + element .querySelectorAll('.measured-template-button') .forEach(element => element.addEventListener('click', renderMeasuredTemplate)); diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index a28725b1..1cce581a 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -1,14 +1,14 @@ -import { diceTypes, getDiceSoNicePresets, range } from '../config/generalConfig.mjs'; +import { diceTypes, getDiceSoNicePresets, getDiceSoNicePreset, range } from '../config/generalConfig.mjs'; import Tagify from '@yaireo/tagify'; export const capitalize = string => { return string.charAt(0).toUpperCase() + string.slice(1); }; -export function rollCommandToJSON(text, raw) { +export function rollCommandToJSON(text) { if (!text) return {}; - const flavorMatch = raw?.match(/{(.*)}$/); + const flavorMatch = text?.match(/{(.*)}$/); const flavor = flavorMatch ? flavorMatch[1] : null; // Match key="quoted string" OR key=unquotedValue @@ -31,7 +31,7 @@ export function rollCommandToJSON(text, raw) { } result[key] = value; } - return Object.keys(result).length > 0 ? { result, flavor } : null; + return { result, flavor }; } export const getCommandTarget = (options = {}) => { @@ -69,6 +69,20 @@ export const setDiceSoNiceForDualityRoll = async (rollResult, advantageState, ho } }; +export const setDiceSoNiceForHopeFateRoll = async (rollResult, hopeFaces) => { + if (!game.modules.get('dice-so-nice')?.active) return; + const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance); + const diceSoNicePresets = await getDiceSoNicePreset(diceSoNice.hope, hopeFaces); + rollResult.dice[0].options = diceSoNicePresets; +}; + +export const setDiceSoNiceForFearFateRoll = async (rollResult, fearFaces) => { + if (!game.modules.get('dice-so-nice')?.active) return; + const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance); + const diceSoNicePresets = await getDiceSoNicePreset(diceSoNice.fear, fearFaces); + rollResult.dice[0].options = diceSoNicePresets; +}; + export const chunkify = (array, chunkSize, mappingFunc) => { var chunkifiedArray = []; for (let i = 0; i < array.length; i += chunkSize) { diff --git a/styles/less/dialog/death-move/death-move-container.less b/styles/less/dialog/death-move/death-move-container.less index e5803d95..903704e3 100644 --- a/styles/less/dialog/death-move/death-move-container.less +++ b/styles/less/dialog/death-move/death-move-container.less @@ -1,55 +1,56 @@ -@import '../../utils/spacing.less'; -@import '../../utils/colors.less'; -@import '../../utils/fonts.less'; - -.daggerheart.dh-style.dialog.death-move { - .death-move-container { - display: flex; - flex-direction: column; - gap: 5px; - - .moves-list { - .move-item { - display: flex; - align-items: center; - gap: 5px; - - &:hover { - background-color: light-dark(@soft-shadow, @soft-white-shadow); - cursor: pointer; - } - padding: 5px; - border-radius: 5px; - transition: background-color 0.3s ease-in-out; - - .label { - display: flex; - align-items: center; - gap: 10px; - cursor: pointer; - flex: 1; - i { - text-align: center; - width: 30px; - } - } - - input[type='radio'] { - margin-left: auto; - } - } - } - } - - footer { - margin-top: 8px; - display: flex; - gap: 8px; - - button { - flex: 1; - height: 40px; - font-weight: 600; - } - } -} +@import '../../utils/spacing.less'; +@import '../../utils/colors.less'; +@import '../../utils/fonts.less'; + +.daggerheart.dh-style.dialog.death-move { + .death-move-container { + display: flex; + flex-direction: column; + gap: 5px; + + .moves-list { + .move-item { + display: flex; + align-items: center; + gap: 5px; + padding: 5px; + border-radius: 5px; + transition: background-color 0.3s ease-in-out; + height: 37px; + + &:hover { + background-color: light-dark(@soft-shadow, @soft-white-shadow); + cursor: pointer; + } + + .label { + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; + flex: 1; + i { + text-align: center; + width: 30px; + } + } + + input[type='radio'] { + margin-left: auto; + } + } + } + } + + footer { + margin-top: 8px; + display: flex; + gap: 8px; + + button { + flex: 1; + height: 40px; + font-weight: 600; + } + } +} diff --git a/styles/less/dialog/index.less b/styles/less/dialog/index.less index b5ed8764..01a3f954 100644 --- a/styles/less/dialog/index.less +++ b/styles/less/dialog/index.less @@ -38,4 +38,6 @@ @import './item-transfer/sheet.less'; -@import './settings/change-currency-icon.less'; \ No newline at end of file +@import './settings/change-currency-icon.less'; + +@import './risk-it-all/sheet.less'; diff --git a/styles/less/dialog/risk-it-all/sheet.less b/styles/less/dialog/risk-it-all/sheet.less new file mode 100644 index 00000000..db34a5c1 --- /dev/null +++ b/styles/less/dialog/risk-it-all/sheet.less @@ -0,0 +1,60 @@ +.daggerheart.dialog.dh-style.views.risk-it-all { + .risk-it-all-container { + display: flex; + align-items: center; + flex-direction: column; + gap: 8px; + text-align: center; + + header { + font-weight: bold; + font-size: var(--font-size-20); + } + + .section-label { + font-size: var(--font-size-18); + text-decoration: underline; + } + + .remaining-section { + display: flex; + flex-direction: column; + gap: 2px; + } + + .resource-section { + width: 100%; + display: flex; + gap: 8px; + } + + .final-section { + width: 100%; + display: flex; + flex-direction: column; + gap: 2px; + + .final-section-values-container { + width: 100%; + display: flex; + align-items: center; + justify-content: space-evenly; + + .final-section-value-container { + display: flex; + flex-direction: column; + gap: 2px; + } + } + } + } + + footer { + width: 100%; + display: flex; + + button { + flex: 1; + } + } +} diff --git a/styles/less/global/enrichment.less b/styles/less/global/enrichment.less index 2ad3975a..8256d60a 100644 --- a/styles/less/global/enrichment.less +++ b/styles/less/global/enrichment.less @@ -1,5 +1,6 @@ .measured-template-button, .enriched-damage-button, +.fate-roll-button, .duality-roll-button { display: inline; diff --git a/styles/less/sheets/actors/character/header.less b/styles/less/sheets/actors/character/header.less index 593f1b73..5e8ef002 100644 --- a/styles/less/sheets/actors/character/header.less +++ b/styles/less/sheets/actors/character/header.less @@ -195,6 +195,11 @@ .hope-value { display: flex; cursor: pointer; + + &.scar { + cursor: initial; + opacity: 0.6; + } } } } diff --git a/styles/less/ui/chat/chat.less b/styles/less/ui/chat/chat.less index 57e9fd57..1b1e3c1c 100644 --- a/styles/less/ui/chat/chat.less +++ b/styles/less/ui/chat/chat.less @@ -14,6 +14,7 @@ color: @dark; } + &.fate, &.duality { background-image: url(../assets/parchments/dh-parchment-dark.png); @@ -66,6 +67,7 @@ } } + &.fate, &.critical { --text-color: @chat-purple; --bg-color: @chat-purple-40; @@ -80,7 +82,7 @@ } } - &:not(.duality) { + &:not(.duality .fate) { .font-20 { color: @dark; } @@ -173,6 +175,28 @@ } } + &.fate { + + &.hope { + --text-color: @golden; + --bg-color: @golden-40; + .message-header, + .message-content { + background-color: @golden-bg; + } + } + + &.fear { + --text-color: @chat-blue; + --bg-color: @chat-blue-40; + .message-header, + .message-content { + background-color: @chat-blue-bg; + } + } + + } + &.duality { &.hope { --text-color: @golden; diff --git a/styles/less/ui/chat/deathmoves.less b/styles/less/ui/chat/deathmoves.less new file mode 100644 index 00000000..175b8753 --- /dev/null +++ b/styles/less/ui/chat/deathmoves.less @@ -0,0 +1,152 @@ +@import '../../utils/colors.less'; +@import '../../utils/fonts.less'; +@import '../../utils/spacing.less'; + +#interface.theme-light { + .daggerheart.chat.death-moves { + .death-moves-list .death-move { + &:hover { + background: @dark-blue-10; + } + + .death-label { + border-bottom: 1px solid @dark-blue; + + .header-label .title { + color: @dark-blue; + } + .header-label .label { + color: @dark; + } + } + + .fa-chevron-down { + color: @dark-blue; + } + } + + .description { + color: @dark; + } + + .result { + color: @dark; + } + + .risk-it-all-button { + color: @dark; + } + } +} + +.daggerheart.chat { + &.death-moves { + display: flex; + flex-direction: column; + align-items: center; + + details[open] { + .fa-chevron-down { + transform: rotate(180deg); + transition: all 0.3s ease; + } + } + + .death-moves-list { + display: flex; + flex-direction: column; + gap: 5px; + width: 100%; + + .fa-chevron-down { + transition: all 0.3s ease; + margin-left: auto; + } + + .death-move { + width: 100%; + + .death-label { + display: flex; + align-items: center; + gap: 5px; + border-bottom: 1px solid @golden; + margin: 0 8px; + padding-bottom: 5px; + width: -webkit-fill-available; + + &:hover { + background: light-dark(@dark-blue-10, @golden-10); + cursor: pointer; + transition: all 0.3s ease; + } + + .death-image { + width: 40px; + height: 40px; + border-radius: 3px; + } + + .header-label { + padding: 8px; + .title { + font-size: var(--font-size-16); + color: @golden; + font-weight: 700; + } + .label { + font-size: var(--font-size-12); + color: @beige; + margin: 0; + } + } + } + + .description { + padding: 8px; + } + } + + .action-use-button-parent { + width: 100%; + + .action-use-target { + display: flex; + align-items: center; + justify-content: space-between; + gap: 4px; + width: 100%; + padding: 4px 8px 10px 40px; + font-size: var(--font-size-12); + + label { + font-weight: bold; + } + + select { + flex: 1; + } + } + } + + .action-use-button { + width: -webkit-fill-available; + margin: 0 8px; + font-weight: 600; + height: 40px; + } + } + + .result { + padding: 8px; + font-weight: bold; + } + + .risk-it-all-button { + width: -webkit-fill-available; + margin: 0 8px; + font-weight: 600; + height: 40px; + } + } +} diff --git a/styles/less/ui/index.less b/styles/less/ui/index.less index 25f51d0f..065e43c5 100644 --- a/styles/less/ui/index.less +++ b/styles/less/ui/index.less @@ -6,6 +6,7 @@ @import './chat/effect-summary.less'; @import './chat/group-roll.less'; @import './chat/refresh-message.less'; +@import './chat/deathmoves.less'; @import './chat/sheet.less'; @import './combat-sidebar/combat-sidebar.less'; diff --git a/system.json b/system.json index 2c68f785..8e4d19d9 100644 --- a/system.json +++ b/system.json @@ -285,6 +285,7 @@ }, "ChatMessage": { "dualityRoll": {}, + "fateRoll": {}, "adversaryRoll": {}, "damageRoll": {}, "abilityUse": {}, diff --git a/templates/dialogs/dice-roll/rollSelection.hbs b/templates/dialogs/dice-roll/rollSelection.hbs index e60f4683..5851a33d 100644 --- a/templates/dialogs/dice-roll/rollSelection.hbs +++ b/templates/dialogs/dice-roll/rollSelection.hbs @@ -68,95 +68,127 @@ {{/if}} {{/if}} {{/if}} + {{#if (eq @root.rollType 'FateRoll')}} + {{#if (eq @root.roll.fateDie 'Hope')}} + +
+ +
+ {{localize "DAGGERHEART.GENERAL.hope"}} + +
+
+ {{/if}} + + {{#if (eq @root.roll.fateDie 'Fear')}} +
+ +
+ {{localize "DAGGERHEART.GENERAL.fear"}} + +
+
+ {{/if}} + + {{/if}} - {{#if hasSelectedEffects}} -
- {{localize "DAGGERHEART.GENERAL.Effect.plural"}} + {{#if (ne @root.rollType 'FateRoll')}} + {{#if hasSelectedEffects}} +
+ {{localize "DAGGERHEART.GENERAL.Effect.plural"}} - {{#each selectedEffects as |effect id|}} -
- - {{effect.name}} -
- {{/each}} -
- {{/if}} - - {{#if experiences.length}} -
- {{localize "DAGGERHEART.GENERAL.experience.plural"}} - {{#each experiences}} - {{#if name}} -
- - {{name}} +{{value}} + {{#each selectedEffects as |effect id|}} +
+ + {{effect.name}}
- {{/if}} - {{/each}} -
- {{/if}} + {{/each}} +
+ {{/if}} -
- {{localize "DAGGERHEART.GENERAL.Modifier.plural"}} -
- - -
- {{#unless (eq @root.rollType 'D20Roll')}} + {{#if experiences.length}} +
+ {{localize "DAGGERHEART.GENERAL.experience.plural"}} + {{#each experiences}} + {{#if name}} +
+ + {{name}} +{{value}} +
+ {{/if}} + {{/each}} +
+ {{/if}} +
+ {{#if @root.advantage}} + {{localize "DAGGERHEART.GENERAL.Modifier.plural"}}
- - + +
- {{#if abilities}} - {{localize "DAGGERHEART.GENERAL.traitModifier"}} - + {{#times 10}} + + {{/times}} + + + + {{#if abilities}} + {{localize "DAGGERHEART.GENERAL.traitModifier"}} + + {{/if}} + {{/unless}} + {{/if}} + {{#if @root.rallyDie.length}} + {{localize "DAGGERHEART.CLASS.Feature.rallyDice"}} + {{/if}} - {{/unless}} - {{#if @root.rallyDie.length}} - {{localize "DAGGERHEART.CLASS.Feature.rallyDice"}} - - {{/if}} - {{#if (eq @root.rollType 'DualityRoll')}}{{localize "DAGGERHEART.GENERAL.situationalBonus"}}{{/if}} - -
+ {{#if (eq @root.rollType 'DualityRoll')}}{{localize "DAGGERHEART.GENERAL.situationalBonus"}}{{/if}} + +
+ {{/if}} {{/unless}} {{#if (or costs uses)}} {{> 'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs'}} {{/if}} - {{localize "DAGGERHEART.GENERAL.formula"}}: {{@root.formula}} + {{#if (ne @root.rollType 'FateRoll')}} + {{localize "DAGGERHEART.GENERAL.formula"}}: {{@root.formula}} + {{/if}}
+
+
+ + +
+ + +
+ +
+
+ + {{this.final.hitPoints.value}}/{{this.final.hitPoints.max}} +
+
+ + {{this.final.stress.value}}/{{this.final.stress.max}} +
+
+
+ +
+ +
+ + \ No newline at end of file diff --git a/templates/settings/homebrew-settings/settings.hbs b/templates/settings/homebrew-settings/settings.hbs index cdcbd461..4b6e7d85 100644 --- a/templates/settings/homebrew-settings/settings.hbs +++ b/templates/settings/homebrew-settings/settings.hbs @@ -8,6 +8,7 @@

{{localize 'DAGGERHEART.SETTINGS.Menu.homebrew.name'}}

{{formGroup settingFields.schema.fields.maxFear value=settingFields._source.maxFear localize=true}} + {{formGroup settingFields.schema.fields.maxHope value=settingFields._source.maxHope localize=true}} {{formGroup settingFields.schema.fields.maxDomains value=settingFields._source.maxDomains localize=true}} {{formGroup settingFields.schema.fields.maxLoadout value=settingFields._source.maxLoadout localize=true}}
diff --git a/templates/sheets-settings/action-settings/trigger.hbs b/templates/sheets-settings/action-settings/trigger.hbs index b048461e..9ef97733 100644 --- a/templates/sheets-settings/action-settings/trigger.hbs +++ b/templates/sheets-settings/action-settings/trigger.hbs @@ -30,7 +30,7 @@
- {{formInput @root.fields.triggers.element.fields.command value=trigger.command elementType="code-mirror" name=(concat "triggers." index ".command") aria=(object label=(localize "Test")) }} + {{formInput @root.fields.triggers.element.fields.command value=trigger.command elementType="code-mirror" name=(concat "triggers." index ".command") }}
{{/each}} diff --git a/templates/sheets-settings/character-settings/details.hbs b/templates/sheets-settings/character-settings/details.hbs index 42e17a9b..3f9247e0 100644 --- a/templates/sheets-settings/character-settings/details.hbs +++ b/templates/sheets-settings/character-settings/details.hbs @@ -31,7 +31,7 @@ {{formGroup systemFields.resources.fields.stress.fields.max value=document._source.system.resources.stress.max localize=true}} {{formGroup systemFields.resources.fields.hope.fields.value value=document._source.system.resources.hope.value localize=true}} - {{formGroup systemFields.resources.fields.hope.fields.max value=document._source.system.resources.hope.max localize=true}} + {{formGroup systemFields.scars value=document._source.system.scars localize=true}} {{formGroup systemFields.proficiency value=document._source.system.proficiency localize=true}} diff --git a/templates/sheets/actors/character/header.hbs b/templates/sheets/actors/character/header.hbs index 87319dbb..d2c01f3c 100644 --- a/templates/sheets/actors/character/header.hbs +++ b/templates/sheets/actors/character/header.hbs @@ -76,6 +76,11 @@ {{/if}} {{/times}} + {{#times document.system.scars}} + + + + {{/times}} {{#if document.system.class.value}}
diff --git a/templates/ui/chat/deathMove.hbs b/templates/ui/chat/deathMove.hbs index 9940376e..7c677fe3 100644 --- a/templates/ui/chat/deathMove.hbs +++ b/templates/ui/chat/deathMove.hbs @@ -1,17 +1,32 @@ -
-
    -
    - - +
    +
      +
      + +

      {{this.title}}

      {{localize 'DAGGERHEART.UI.Chat.deathMove.title'}}
      - +
      {{{this.description}}}
    +
    + {{{this.result}}} +
    + {{#if this.showRiskItAllButton}} +
    + +
    + {{/if}} + +
    +
    \ No newline at end of file diff --git a/templates/ui/chat/parts/roll-part.hbs b/templates/ui/chat/parts/roll-part.hbs index 8e88015f..ea962f9e 100644 --- a/templates/ui/chat/parts/roll-part.hbs +++ b/templates/ui/chat/parts/roll-part.hbs @@ -29,64 +29,86 @@
    - {{#if roll.hope}} -
    - -
    - {{#if roll.hope.rerolled.any}}{{/if}} - {{roll.hope.value}} -
    -
    -
    - -
    - {{#if roll.fear.rerolled.any}}{{/if}} - {{roll.fear.value}} -
    -
    - {{#if roll.advantage.type}} -
    - {{#if (eq roll.advantage.type 1)}} - -
    {{roll.advantage.value}}
    - {{else}} - -
    {{roll.advantage.value}}
    - {{/if}} -
    - {{/if}} - {{#if roll.rally.dice}} -
    - -
    {{roll.rally.value}}
    -
    - {{/if}} - {{#each roll.extra}} - {{#each results}} - {{#unless discarded}} -
    - -
    {{result}}
    -
    - {{/unless}} - {{/each}} - {{/each}} - {{else}} - {{#each roll.dice}} - {{#each results}} -
    -
    - {{result}} -
    + {{#if roll.fate}} + {{#if (eq roll.fate.fateDie "Hope")}} +
    + +
    + {{roll.fate.value}}
    +
    + {{/if}} + {{#if (eq roll.fate.fateDie "Fear")}} +
    + +
    + {{roll.fate.value}} +
    +
    + {{/if}} + {{else}} + {{#if roll.hope}} +
    + +
    + {{#if roll.hope.rerolled.any}}{{/if}} + {{roll.hope.value}} +
    +
    +
    + +
    + {{#if roll.fear.rerolled.any}}{{/if}} + {{roll.fear.value}} +
    +
    + {{#if roll.advantage.type}} +
    + {{#if (eq roll.advantage.type 1)}} + +
    {{roll.advantage.value}}
    + {{else}} + +
    {{roll.advantage.value}}
    + {{/if}} +
    + {{/if}} + {{#if roll.rally.dice}} +
    + +
    {{roll.rally.value}}
    +
    + {{/if}} + {{#each roll.extra}} + {{#each results}} + {{#unless discarded}} +
    + +
    {{result}}
    +
    + {{/unless}} + {{/each}} {{/each}} - {{/each}} + {{else}} + {{#each roll.dice}} + {{#each results}} +
    +
    + {{result}} +
    +
    + {{/each}} + {{/each}} + {{/if}} {{/if}}
    -
    {{roll.formula}}
    + {{#if roll.fate}} + {{else}} +
    {{roll.formula}}
    + {{/if}}
{{/unless}} -
\ No newline at end of file + 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 26/48] 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 27/48] [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 28/48] [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 6e5d7fb34c85ff77449e60f39343126d0a49289c Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Wed, 21 Jan 2026 02:52:42 +0100 Subject: [PATCH 29/48] Fixed so that saving throw damage mitigation works again (#1555) --- module/data/action/baseAction.mjs | 1 + module/documents/actor.mjs | 2 +- module/documents/chatMessage.mjs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index b5f95aff..c403d4a9 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -241,6 +241,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel hasHealing: this.hasHealing, hasEffect: this.hasEffect, hasSave: this.hasSave, + onSave: this.save?.damageMod, isDirect: !!this.damage?.direct, selectedRollMode: game.settings.get('core', 'rollMode'), data: this.getRollData(), diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 27c310ae..6d943c54 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -607,7 +607,7 @@ export default class DhpActor extends Actor { if (!updates.length) return; const hpDamage = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id); - if (hpDamage) { + if (hpDamage?.value) { hpDamage.value = this.convertDamageToThreshold(hpDamage.value); if ( this.type === 'character' && diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index 2f23cc1a..4c6b0b93 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -179,7 +179,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { config = foundry.utils.deepClone(this.system); config.event = event; - if (this.system.onSave) { + if (config.hasSave) { const pendingingSaves = targets.filter(t => t.saved.success === null); if (pendingingSaves.length) { const confirm = await foundry.applications.api.DialogV2.confirm({ From f659d08d58c236256f9d8d9ae48005e80594b7ff Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Wed, 21 Jan 2026 02:53:07 +0100 Subject: [PATCH 30/48] . (#1563) --- module/applications/ui/combatTracker.mjs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/module/applications/ui/combatTracker.mjs b/module/applications/ui/combatTracker.mjs index 288ba8ad..fc47f085 100644 --- a/module/applications/ui/combatTracker.mjs +++ b/module/applications/ui/combatTracker.mjs @@ -42,8 +42,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C this.combats .find(x => x.active) ?.system?.extendedBattleToggles?.reduce((acc, toggle) => (acc ?? 0) + toggle.category, null) ?? null; - const maxBP = CONFIG.DH.ENCOUNTER.BaseBPPerEncounter(context.characters.length) + modifierBP; - const currentBP = AdversaryBPPerEncounter(context.adversaries, context.characters); + const maxBP = CONFIG.DH.ENCOUNTER.BaseBPPerEncounter(context.allCharacters.length) + modifierBP; + const currentBP = AdversaryBPPerEncounter(context.adversaries, context.allCharacters); Object.assign(context, { fear: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), @@ -73,9 +73,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C Object.assign(context, { actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens, adversaries, - characters: characters - ?.filter(x => !x.isNPC) - .filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0), + allCharacters: characters, + characters: characters.filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0), spotlightRequests }); } 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 31/48] 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 38fb00bd1058c24af88475157ade74996f0ddfef Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:47:03 +0100 Subject: [PATCH 32/48] Fixed DowntimeMoves and ItemFeatures reset functions (#1568) --- lang/en.json | 1 + .../settings/homebrewSettings.mjs | 32 +++++++++++++++++-- .../settings/homebrew-settings/downtime.hbs | 4 +-- .../homebrew-settings/itemFeatures.hbs | 4 +-- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/lang/en.json b/lang/en.json index 8e64ab7d..18048d9d 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2506,6 +2506,7 @@ "itemFeatures": "Item Features", "nrChoices": "# Moves Per Rest", "resetMovesTitle": "Reset {type} Downtime Moves", + "resetItemFeaturesTitle": "Reset {type}", "resetMovesText": "Are you sure you want to reset?", "FIELDS": { "maxFear": { "label": "Max Fear" }, diff --git a/module/applications/settings/homebrewSettings.mjs b/module/applications/settings/homebrewSettings.mjs index 3c4486c1..6e2e665d 100644 --- a/module/applications/settings/homebrewSettings.mjs +++ b/module/applications/settings/homebrewSettings.mjs @@ -36,7 +36,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli addItem: this.addItem, editItem: this.editItem, removeItem: this.removeItem, - resetMoves: this.resetMoves, + resetDowntimeMoves: this.resetDowntimeMoves, + resetItemFeatures: this.resetItemFeatures, addDomain: this.addDomain, toggleSelectedDomain: this.toggleSelectedDomain, deleteDomain: this.deleteDomain, @@ -232,7 +233,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli this.render(); } - static async resetMoves(_, target) { + static async resetDowntimeMoves(_, target) { const confirmed = await foundry.applications.api.DialogV2.confirm({ window: { title: game.i18n.format('DAGGERHEART.SETTINGS.Homebrew.resetMovesTitle', { @@ -266,7 +267,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli ...move, name: game.i18n.localize(move.name), description: game.i18n.localize(move.description), - actions: move.actions.reduce((acc, key) => { + actions: Object.keys(move.actions).reduce((acc, key) => { const action = move.actions[key]; acc[key] = { ...action, @@ -293,6 +294,31 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli this.render(); } + static async resetItemFeatures(_, target) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.format('DAGGERHEART.SETTINGS.Homebrew.resetItemFeaturesTitle', { + type: game.i18n.localize(`DAGGERHEART.GENERAL.${target.dataset.type}`) + }) + }, + content: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.resetMovesText') + }); + + if (!confirmed) return; + + await this.settings.updateSource({ + [`itemFeatures.${target.dataset.type}`]: Object.keys( + this.settings.itemFeatures[target.dataset.type] + ).reduce((acc, key) => { + acc[`-=${key}`] = null; + + return acc; + }, {}) + }); + + this.render(); + } + static async addDomain(event) { event.preventDefault(); const content = new foundry.data.fields.StringField({ diff --git a/templates/settings/homebrew-settings/downtime.hbs b/templates/settings/homebrew-settings/downtime.hbs index 8612f3d5..25f4c95d 100644 --- a/templates/settings/homebrew-settings/downtime.hbs +++ b/templates/settings/homebrew-settings/downtime.hbs @@ -10,7 +10,7 @@ {{localize "DAGGERHEART.APPLICATIONS.Downtime.longRest.title"}} - +
@@ -31,7 +31,7 @@ {{localize "DAGGERHEART.APPLICATIONS.Downtime.shortRest.title"}} - +
diff --git a/templates/settings/homebrew-settings/itemFeatures.hbs b/templates/settings/homebrew-settings/itemFeatures.hbs index 22c23af6..df3419fa 100644 --- a/templates/settings/homebrew-settings/itemFeatures.hbs +++ b/templates/settings/homebrew-settings/itemFeatures.hbs @@ -8,7 +8,7 @@ {{localize "DAGGERHEART.GENERAL.weaponFeatures"}} - +
@@ -22,7 +22,7 @@ {{localize "DAGGERHEART.GENERAL.armorFeatures"}} - +
From c90875fa7cb14553fd223362433daf186af3c11c Mon Sep 17 00:00:00 2001 From: WBHarry Date: Wed, 21 Jan 2026 14:05:39 +0100 Subject: [PATCH 33/48] 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 1f7d4d6f1e864fe1fd38aa59601a52f0e28c6553 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:52:22 +0100 Subject: [PATCH 34/48] Fixed an error where a player having their token initially selected caused an error in effectsDisplay.mjs (#1569) --- module/applications/ui/effectsDisplay.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/module/applications/ui/effectsDisplay.mjs b/module/applications/ui/effectsDisplay.mjs index 0875e783..8c0c939c 100644 --- a/module/applications/ui/effectsDisplay.mjs +++ b/module/applications/ui/effectsDisplay.mjs @@ -76,6 +76,8 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica }; toggleHidden(token, focused) { + if (!this.element) return; + const effects = DhEffectsDisplay.getTokenEffects(focused ? token : null); this.element.hidden = effects.length === 0; From fa3b3fa0a81401ce6e8022582d7bb096004d4806 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Thu, 22 Jan 2026 23:21:42 +0100 Subject: [PATCH 35/48] [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 --- lang/en.json | 1 + module/dice/dhRoll.mjs | 2 +- system.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lang/en.json b/lang/en.json index 18048d9d..33f8c514 100755 --- a/lang/en.json +++ b/lang/en.json @@ -607,6 +607,7 @@ }, "RerollDialog": { "title": "Reroll", + "damageTitle": "Reroll Damage", "deselectDiceNotification": "Deselect one of the selected dice first", "acceptCurrentRolls": "Accept Current Rolls" }, diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index a5ac5091..1977c7ea 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -3,7 +3,7 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; export default class DHRoll extends Roll { baseTerms = []; constructor(formula, data = {}, options = {}) { - super(formula, data, options); + super(formula, data, foundry.utils.mergeObject(options, { roll: [] }, { overwrite: false })); options.bonusEffects = this.bonusEffectBuilder(); if (!this.data || !Object.keys(this.data).length) this.data = options.data; } diff --git a/system.json b/system.json index 2c68f785..6521c6d6 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.5.4", + "version": "1.5.5", "compatibility": { "minimum": "13.346", "verified": "13.351", From 21ef288283bef655918412b25fe4bf692e1e97d1 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Fri, 23 Jan 2026 11:51:14 +0100 Subject: [PATCH 36/48] Fixed when users drag in compendium environments to the sceneEnvironments (#1573) --- .../applications/scene/sceneConfigSettings.mjs | 13 ++++++++++++- module/systemRegistration/migrations.mjs | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/module/applications/scene/sceneConfigSettings.mjs b/module/applications/scene/sceneConfigSettings.mjs index 8a58db5c..98e18f09 100644 --- a/module/applications/scene/sceneConfigSettings.mjs +++ b/module/applications/scene/sceneConfigSettings.mjs @@ -65,8 +65,15 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const item = await foundry.utils.fromUuid(data.uuid); if (item instanceof game.system.api.documents.DhpActor && item.type === 'environment') { + let sceneUuid = data.uuid; + if (item.pack) { + const inWorldActor = await game.system.api.documents.DhpActor.create([item.toObject()]); + if (!inWorldActor.length) return; + sceneUuid = inWorldActor[0].uuid; + } + await this.daggerheartFlag.updateSource({ - sceneEnvironments: [...this.daggerheartFlag.sceneEnvironments, data.uuid] + sceneEnvironments: [...this.daggerheartFlag.sceneEnvironments, sceneUuid] }); this.render({ internalRefresh: true }); } @@ -97,6 +104,10 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S /** @override */ async _processSubmitData(event, form, submitData, options) { submitData.flags.daggerheart = this.daggerheartFlag.toObject(); + submitData.flags.daggerheart.sceneEnvironments = submitData.flags.daggerheart.sceneEnvironments.filter(x => + foundry.utils.fromUuidSync(x) + ); + for (const key of Object.keys(this.document._source.flags.daggerheart?.sceneEnvironments ?? {})) { if (!submitData.flags.daggerheart.sceneEnvironments[key]) { submitData.flags.daggerheart.sceneEnvironments[`-=${key}`] = null; diff --git a/module/systemRegistration/migrations.mjs b/module/systemRegistration/migrations.mjs index b3116459..086647bf 100644 --- a/module/systemRegistration/migrations.mjs +++ b/module/systemRegistration/migrations.mjs @@ -210,6 +210,22 @@ export async function runMigrations() { lastMigrationVersion = '1.2.7'; } + + if (foundry.utils.isNewerVersion('1.5.5', lastMigrationVersion)) { + for (const scene of game.scenes) { + if (!scene.flags.daggerheart) continue; + const systemData = new game.system.api.data.scenes.DHScene(scene.flags.daggerheart); + const sceneEnvironments = systemData.sceneEnvironments; + + const newEnvironments = sceneEnvironments.filter(x => !x?.pack); + if (newEnvironments.length !== sceneEnvironments.length) + await scene.update({ 'flags.daggerheart.sceneEnvironments': newEnvironments }); + } + + ui.nav.render(true); + + lastMigrationVersion = '1.5.5'; + } //#endregion await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, lastMigrationVersion); 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 37/48] [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 38/48] [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); From cb998860d91cad3fa4d295677b78606ca156116c Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:19:16 +0100 Subject: [PATCH 39/48] Spellcastmodifiers were not being sorted correctly for use (#1578) --- module/data/actor/character.mjs | 2 +- templates/sheets/actors/character/sidebar.hbs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 12396384..47660da4 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -368,7 +368,7 @@ export default class DhCharacter extends BaseDataActor { const modifiers = subClasses ?.map(sc => ({ ...this.traits[sc.system.spellcastingTrait], key: sc.system.spellcastingTrait })) .filter(x => x); - return modifiers.sort((a, b) => a.value - b.value)[0]; + return modifiers.sort((a, b) => (b.value ?? 0) - (a.value ?? 0))[0]; } get spellcastModifier() { diff --git a/templates/sheets/actors/character/sidebar.hbs b/templates/sheets/actors/character/sidebar.hbs index 0db2bf42..24e68e02 100644 --- a/templates/sheets/actors/character/sidebar.hbs +++ b/templates/sheets/actors/character/sidebar.hbs @@ -1,12 +1,12 @@