From 7d5cdeb09ddafcfefff618198703b232fa9b4f17 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Thu, 16 Apr 2026 02:26:39 +0200 Subject: [PATCH] [Feature] Active Party (#1803) --- lang/en.json | 4 + .../sidebar/tabs/actorDirectory.mjs | 93 +++++++++++-------- module/config/settingsConfig.mjs | 3 +- module/data/actor/party.mjs | 20 ++++ module/data/fields/action/summonField.mjs | 14 +-- module/documents/actor.mjs | 5 +- .../documents/collections/actorCollection.mjs | 7 ++ module/helpers/utils.mjs | 8 +- module/systemRegistration/settings.mjs | 7 ++ ..._Undefeated_Champion_RXkZTwBRi4dJ3JE5.json | 40 ++++---- ...ment_Abandoned_Grove_pGEdzdLkqYtBhxnG.json | 45 +++++++-- .../ui/sidebar/actor-document-partial.hbs | 4 + 12 files changed, 179 insertions(+), 71 deletions(-) diff --git a/lang/en.json b/lang/en.json index bc0cf05a..5c0c7470 100755 --- a/lang/en.json +++ b/lang/en.json @@ -236,6 +236,8 @@ }, "defaultHopeDice": "Default Hope Dice", "defaultFearDice": "Default Fear Dice", + "defaultAdvantageDice": "Default Advantage Dice", + "defaultDisadvantageDice": "Default Disadvantage Dice", "disadvantageSources": { "label": "Disadvantage Sources", "hint": "Add single words or short text as reminders and hints of what a character has disadvantage on." @@ -3210,6 +3212,8 @@ "companion": "Level {level} - {partner}", "companionNoPartner": "No Partner", "duplicateToNewTier": "Duplicate to New Tier", + "activateParty": "Make Active Party", + "partyIsActive": "Active", "createAdversary": "Create Adversary", "pickTierTitle": "Pick a new tier for this adversary" }, diff --git a/module/applications/sidebar/tabs/actorDirectory.mjs b/module/applications/sidebar/tabs/actorDirectory.mjs index e9484553..1306de61 100644 --- a/module/applications/sidebar/tabs/actorDirectory.mjs +++ b/module/applications/sidebar/tabs/actorDirectory.mjs @@ -46,50 +46,67 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs. _getEntryContextOptions() { const options = super._getEntryContextOptions(); - options.push({ - name: 'DAGGERHEART.UI.Sidebar.actorDirectory.duplicateToNewTier', - icon: ``, - condition: li => { - const actor = game.actors.get(li.dataset.entryId); - return actor?.type === 'adversary' && actor.system.type !== 'social'; - }, - callback: async li => { - const actor = game.actors.get(li.dataset.entryId); - if (!actor) throw new Error('Unexpected missing actor'); + options.push( + { + name: 'DAGGERHEART.UI.Sidebar.actorDirectory.duplicateToNewTier', + icon: ``, + condition: li => { + const actor = game.actors.get(li.dataset.entryId); + return actor?.type === 'adversary' && actor.system.type !== 'social'; + }, + callback: async li => { + const actor = game.actors.get(li.dataset.entryId); + if (!actor) throw new Error('Unexpected missing actor'); - const tiers = [1, 2, 3, 4].filter(t => t !== actor.system.tier); - const content = document.createElement('div'); - const select = document.createElement('select'); - select.name = 'tier'; - select.append( - ...tiers.map(t => { - const option = document.createElement('option'); - option.value = t; - option.textContent = game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${t}`); - return option; - }) - ); - content.append(select); + const tiers = [1, 2, 3, 4].filter(t => t !== actor.system.tier); + const content = document.createElement('div'); + const select = document.createElement('select'); + select.name = 'tier'; + select.append( + ...tiers.map(t => { + const option = document.createElement('option'); + option.value = t; + option.textContent = game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${t}`); + return option; + }) + ); + content.append(select); - const tier = await foundry.applications.api.Dialog.input({ - classes: ['dh-style', 'dialog'], - window: { title: 'DAGGERHEART.UI.Sidebar.actorDirectory.pickTierTitle' }, - content, - ok: { - label: 'DAGGERHEART.UI.Sidebar.actorDirectory.createAdversary', - callback: (event, button, dialog) => Number(button.form.elements.tier.value) + const tier = await foundry.applications.api.Dialog.input({ + classes: ['dh-style', 'dialog'], + window: { title: 'DAGGERHEART.UI.Sidebar.actorDirectory.pickTierTitle' }, + content, + ok: { + label: 'DAGGERHEART.UI.Sidebar.actorDirectory.createAdversary', + callback: (event, button, dialog) => Number(button.form.elements.tier.value) + } + }); + + if (tier === actor.system.tier) { + ui.notifications.warn('This actor is already at this tier'); + } else if (tier) { + const source = actor.system.adjustForTier(tier); + await Actor.create(source); + ui.notifications.info(`Tier ${tier} ${actor.name} created`); } - }); + } + }, + { + name: 'DAGGERHEART.UI.Sidebar.actorDirectory.activateParty', + icon: ``, + condition: li => { + const actor = game.actors.get(li.dataset.entryId); + return actor && actor.type === 'party' && !actor.system.active; + }, + callback: async li => { + const actor = game.actors.get(li.dataset.entryId); + if (!actor) throw new Error('Unexpected missing actor'); - if (tier === actor.system.tier) { - ui.notifications.warn('This actor is already at this tier'); - } else if (tier) { - const source = actor.system.adjustForTier(tier); - await Actor.create(source); - ui.notifications.info(`Tier ${tier} ${actor.name} created`); + await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.ActiveParty, actor.id); + ui.actors.render(); } } - }); + ); return options; } diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index 74315a8b..12e8536e 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -40,7 +40,8 @@ export const gameSettings = { LastMigrationVersion: 'LastMigrationVersion', SpotlightRequestQueue: 'SpotlightRequestQueue', CompendiumBrowserSettings: 'CompendiumBrowserSettings', - SpotlightTracker: 'SpotlightTracker' + SpotlightTracker: 'SpotlightTracker', + ActiveParty: 'ActiveParty', }; export const actionAutomationChoices = { diff --git a/module/data/actor/party.mjs b/module/data/actor/party.mjs index c9b99dcd..ea09c622 100644 --- a/module/data/actor/party.mjs +++ b/module/data/actor/party.mjs @@ -18,6 +18,10 @@ export default class DhParty extends BaseDataActor { }; } + get active() { + return game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.ActiveParty) === this.parent.id; + } + /* -------------------------------------------- */ /**@inheritdoc */ @@ -40,6 +44,16 @@ export default class DhParty extends BaseDataActor { } } + _onCreate(data, options, userId) { + super._onCreate(data, options, userId); + + if (game.user.isActiveGM && !game.actors.party) { + game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.ActiveParty, this.parent.id).then(_ => { + ui.actors.render(); + }); + } + } + _onDelete(options, userId) { super._onDelete(options, userId); @@ -47,5 +61,11 @@ export default class DhParty extends BaseDataActor { for (const member of this.partyMembers) { member?.parties?.delete(this.parent); } + + // If this *was* the active party, delete it. We can't use game.actors.party as this actor was already deleted + const isWorldActor = !this.parent?.parent && !this.parent.compendium; + const activePartyId = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.ActiveParty); + if (isWorldActor && this.id === activePartyId) + game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.ActiveParty, null); } } diff --git a/module/data/fields/action/summonField.mjs b/module/data/fields/action/summonField.mjs index 36ea1010..914a1d69 100644 --- a/module/data/fields/action/summonField.mjs +++ b/module/data/fields/action/summonField.mjs @@ -1,3 +1,4 @@ +import { itemAbleRollParse } from '../../../helpers/utils.mjs'; import FormulaField from '../formulaField.mjs'; const fields = foundry.data.fields; @@ -36,13 +37,12 @@ export default class DHSummonField extends fields.ArrayField { 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 roll = new Roll(itemAbleRollParse(summon.count, this.actor, this.item)); + await roll.evaluate(); + const count = roll.total; + if (!roll.isDeterministic && game.modules.get('dice-so-nice')?.active) + rolls.push(roll); + const actor = await 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. */ diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 3e3dfde4..8ae2e062 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -117,7 +117,9 @@ export default class DhpActor extends Actor { } } - async _preDelete() { + async _preDelete(options, user) { + if ((await super._preDelete(options, user)) === false) return false; + if (this.prototypeToken.actorLink) { game.system.registeredTriggers.unregisterItemTriggers(this.items); } else { @@ -600,6 +602,7 @@ export default class DhpActor extends Actor { rollData.system = this.system.getRollData(); rollData.prof = this.system.proficiency ?? 1; rollData.cast = this.system.spellcastModifier ?? 1; + return rollData; } diff --git a/module/documents/collections/actorCollection.mjs b/module/documents/collections/actorCollection.mjs index a3714b30..8df407e6 100644 --- a/module/documents/collections/actorCollection.mjs +++ b/module/documents/collections/actorCollection.mjs @@ -1,4 +1,11 @@ export default class DhActorCollection extends foundry.documents.collections.Actors { + /** @returns the active party */ + get party() { + const id = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.ActiveParty); + const actor = game.actors.get(id); + return actor?.type === "party" ? actor : null; + } + /** Ensure companions are initialized after all other subtypes. */ _initialize() { super._initialize(); diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 131f94b7..4527da1a 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -189,7 +189,13 @@ export const getDeleteKeys = (property, innerProperty, innerPropertyDefaultValue // Fix on Foundry native formula replacement for DH const nativeReplaceFormulaData = Roll.replaceFormulaData; -Roll.replaceFormulaData = function (formula, data = {}, { missing, warn = false } = {}) { +Roll.replaceFormulaData = function (formula, baseData = {}, { missing, warn = false } = {}) { + /* Inserting global data */ + const data = { + ...baseData, + partySize: game.actors?.party?.system.partyMembers.length ?? 0 + }; + const terms = Object.keys(CONFIG.DH.GENERAL.multiplierTypes).map(type => { return { term: type, default: 1 }; }); diff --git a/module/systemRegistration/settings.mjs b/module/systemRegistration/settings.mjs index 63611cda..ae78e23b 100644 --- a/module/systemRegistration/settings.mjs +++ b/module/systemRegistration/settings.mjs @@ -189,4 +189,11 @@ const registerNonConfigSettings = () => { config: false, type: SpotlightTracker }); + + game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.ActiveParty, { + scope: 'world', + config: false, + type: String, + default: null, + }); }; diff --git a/src/packs/adversaries/adversary_Fallen_Warlord__Undefeated_Champion_RXkZTwBRi4dJ3JE5.json b/src/packs/adversaries/adversary_Fallen_Warlord__Undefeated_Champion_RXkZTwBRi4dJ3JE5.json index 5ad77ab0..63d9ca2c 100644 --- a/src/packs/adversaries/adversary_Fallen_Warlord__Undefeated_Champion_RXkZTwBRi4dJ3JE5.json +++ b/src/packs/adversaries/adversary_Fallen_Warlord__Undefeated_Champion_RXkZTwBRi4dJ3JE5.json @@ -174,12 +174,9 @@ "src": "systems/daggerheart/assets/icons/documents/actors/dragon-head.svg", "anchorX": 0.5, "anchorY": 0.5, - "offsetX": 0, - "offsetY": 0, "fit": "contain", "scaleX": 1, "scaleY": 1, - "rotation": 0, "tint": "#ffffff", "alphaThreshold": 0.75 }, @@ -230,7 +227,7 @@ "saturation": 0, "contrast": 0 }, - "detectionModes": [], + "detectionModes": {}, "occludable": { "radius": 0 }, @@ -256,7 +253,8 @@ "flags": {}, "randomImg": false, "appendNumber": false, - "prependAdjective": false + "prependAdjective": false, + "depth": 1 }, "items": [ { @@ -496,34 +494,42 @@ "description": "

Spend a Fear to summon a number of @UUID[Compendium.daggerheart.adversaries.Actor.OsLG2BjaEdTZUJU9]{Fallen Shock Troops} equal to twice the number of PCs. The Shock Troops appear at Far range.

", "resource": null, "actions": { - "hGMzqw00JTlYfHYy": { - "type": "effect", - "_id": "hGMzqw00JTlYfHYy", + "SrU7qbh8LcOgfozT": { + "type": "summon", + "_id": "SrU7qbh8LcOgfozT", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", + "triggers": [], "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.OsLG2BjaEdTZUJU9", + "count": "@partySize*2" + } + ], "name": "Spend Fear", - "img": "icons/magic/death/undead-skeleton-worn-blue.webp", - "range": "" + "range": "far" } }, "originItemType": null, diff --git a/src/packs/environments/environment_Abandoned_Grove_pGEdzdLkqYtBhxnG.json b/src/packs/environments/environment_Abandoned_Grove_pGEdzdLkqYtBhxnG.json index 75fc932f..0cab16a2 100644 --- a/src/packs/environments/environment_Abandoned_Grove_pGEdzdLkqYtBhxnG.json +++ b/src/packs/environments/environment_Abandoned_Grove_pGEdzdLkqYtBhxnG.json @@ -52,12 +52,9 @@ "src": "systems/daggerheart/assets/icons/documents/actors/forest.svg", "anchorX": 0.5, "anchorY": 0.5, - "offsetX": 0, - "offsetY": 0, "fit": "contain", "scaleX": 1, "scaleY": 1, - "rotation": 0, "tint": "#ffffff", "alphaThreshold": 0.75 }, @@ -108,7 +105,7 @@ "saturation": 0, "contrast": 0 }, - "detectionModes": [], + "detectionModes": {}, "occludable": { "radius": 0 }, @@ -134,7 +131,8 @@ "flags": {}, "randomImg": false, "appendNumber": false, - "prependAdjective": false + "prependAdjective": false, + "depth": 1 }, "items": [ { @@ -323,7 +321,42 @@ "system": { "description": "

A @UUID[Compendium.daggerheart.adversaries.Actor.8yUj2Mzvnifhxegm]{Young Dryad}, two @UUID[Compendium.daggerheart.adversaries.Actor.VtFBt9XBE0WrGGxP]{Sylvan Soldiers}, and a number of @UUID[Compendium.daggerheart.adversaries.Actor.G62k4oSkhkoXEs2D]{Minor Treants} equal to the number of PCs appear to confront the party for their intrusion.

What are the grove guardians concealing? What threat to the forest could the PCs confront to appease the Dryad?

", "resource": null, - "actions": {}, + "actions": { + "TPm6rpKA4mbili82": { + "type": "summon", + "_id": "TPm6rpKA4mbili82", + "systemPath": "actions", + "baseAction": false, + "description": "", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "triggers": [], + "cost": [], + "uses": { + "value": null, + "max": null, + "recovery": null, + "consumeOnSuccess": false + }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.8yUj2Mzvnifhxegm", + "count": "1" + }, + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.VtFBt9XBE0WrGGxP", + "count": "2" + }, + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.G62k4oSkhkoXEs2D", + "count": "@partySize" + } + ] + } + }, "originItemType": null, "originId": null, "featureForm": "action" diff --git a/templates/ui/sidebar/actor-document-partial.hbs b/templates/ui/sidebar/actor-document-partial.hbs index ec261f85..2a9f47fa 100644 --- a/templates/ui/sidebar/actor-document-partial.hbs +++ b/templates/ui/sidebar/actor-document-partial.hbs @@ -14,6 +14,10 @@ {{else}} {{localize "DAGGERHEART.UI.Sidebar.actorDirectory.companionNoPartner"}} {{/if}} + {{else if (eq type "party")}} + {{#if system.active}} + {{localize "DAGGERHEART.UI.Sidebar.actorDirectory.partyIsActive"}} + {{/if}} {{/if}}