Skip to content

Commit

Permalink
[#3028] Clear dependent effects when concentration is broken.
Browse files Browse the repository at this point in the history
  • Loading branch information
Fyorl committed Mar 20, 2024
1 parent 0d9900e commit 6bde97a
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 18 deletions.
4 changes: 3 additions & 1 deletion lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@
"DND5E.Concentration": "Concentration",
"DND5E.ConcentrationAbbr": "C",
"DND5E.ConcentrationBreak": "Break Concentration",
"DND5E.ConcentrationBreakWarning": "Breaking concentration on an effect that is active on other creatures requires an active GM to be present.",
"DND5E.ConcentrationBonus": "Concentration Bonus",
"DND5E.ConcentrationDuration": "Concentration, up to {duration}",
"DND5E.ConcentratingOn": "You are maintaining concentration on the effects of the '{name}' {type}.",
Expand Down Expand Up @@ -668,7 +669,8 @@
"DND5E.Effect": "Effect",
"DND5E.Effects": "Effects",
"DND5E.EffectsApplyTokens": "Apply to selected tokens",
"DND5E.EffectApplyWarning": "Effects cannot be applied to tokens you are not the owner of.",
"DND5E.EffectApplyWarningConcentration": "Applying an effect that is being concentrated on by another character requires GM permissions.",
"DND5E.EffectApplyWarningOwnership": "Effects cannot be applied to tokens you are not the owner of.",
"DND5E.EffectsSearch": "Search effects",
"DND5E.Environment": "Environment",
"DND5E.EquipmentBonus": "Magical Bonus",
Expand Down
47 changes: 47 additions & 0 deletions module/documents/active-effect.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,26 @@ export default class ActiveEffect5e extends ActiveEffect {

/* -------------------------------------------- */

/** @inheritDoc */
async _preDelete(options, user) {
const dependents = this.getDependents();
if ( dependents.length && !game.users.activeGM ) {
ui.notifications.warn("DND5E.ConcentrationBreakWarning", { localize: true });
return false;
}
return super._preDelete(options, user);
}

/* -------------------------------------------- */

/** @inheritDoc */
_onDelete(options, userId) {
super._onDelete(options, userId);
if ( game.user === game.users.activeGM ) this.getDependents().forEach(e => e.delete());
}

/* -------------------------------------------- */

/**
* Prepare effect favorite data.
* @returns {Promise<FavoriteData5e>}
Expand Down Expand Up @@ -531,6 +551,33 @@ export default class ActiveEffect5e extends ActiveEffect {
});
}

/* -------------------------------------------- */

/**
* Record another effect as a dependent of this one.
* @param {ActiveEffect5e} dependent The dependent effect.
* @returns {Promise<ActiveEffect5e>}
*/
addDependent(dependent) {
const dependents = this.getFlag("dnd5e", "dependents") ?? [];
dependents.push({ uuid: dependent.uuid });
return this.setFlag("dnd5e", "dependents", dependents);
}

/* -------------------------------------------- */

/**
* Retrieve a list of dependent effects.
* @returns {ActiveEffect5e[]}
*/
getDependents() {
return (this.getFlag("dnd5e", "dependents") || []).reduce((arr, { uuid }) => {
const effect = fromUuidSync(uuid);
if ( effect ) arr.push(effect);
return arr;
}, []);
}

/* -------------------------------------------- */
/* Deprecations */
/* -------------------------------------------- */
Expand Down
52 changes: 35 additions & 17 deletions module/documents/item.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1022,21 +1022,23 @@ export default class Item5e extends SystemDocumentMixin(Item) {
if ( !foundry.utils.isEmpty(actorUpdates) ) await this.actor.update(actorUpdates);
if ( !foundry.utils.isEmpty(resourceUpdates) ) await this.actor.updateEmbeddedDocuments("Item", resourceUpdates);

// Prepare card data & display it if options.createMessage is true
const cardData = await item.displayCard(options);

// Initiate or concentration.
// Initiate or end concentration.
const effects = [];
if ( config.beginConcentrating ) {
const effect = await item.actor.beginConcentrating(item);
if ( effect ) effects.push(effect);

if ( effect ) {
effects.push(effect);
foundry.utils.setProperty(options.flags, "dnd5e.use.concentrationId", effect.id);
}
if ( config.endConcentration ) {
const deleted = await item.actor.endConcentration(config.endConcentration);
effects.push(...deleted);
}
}

// Prepare card data & display it if options.createMessage is true
const cardData = await item.displayCard(options);

// Initiate measured template creation
let templates;
if ( config.createMeasuredTemplate ) {
Expand Down Expand Up @@ -2019,11 +2021,14 @@ export default class Item5e extends SystemDocumentMixin(Item) {
const li = button.closest("li.effect");
let effect = item.effects.get(li.dataset.effectId);
if ( !effect ) effect = await fromUuid(li.dataset.uuid);
let warn = false;
const concentration = actor.effects.get(message.getFlag("dnd5e", "use.concentrationId"));
for ( const token of canvas.tokens.controlled ) {
if ( await this._applyEffectToToken(effect, token) === false ) warn = true;
try {
await this._applyEffectToToken(effect, token, { concentration });
} catch(err) {
Hooks.onError("Item5e._applyEffectToToken", err, { notify: "warn", log: "warn" });
}
}
if ( warn ) ui.notifications.warn("DND5E.EffectApplyWarning", { localize: true });
break;
case "attack":
await item.rollAttack({
Expand Down Expand Up @@ -2084,30 +2089,43 @@ export default class Item5e extends SystemDocumentMixin(Item) {

/**
* Handle applying an Active Effect to a Token.
* @param {ActiveEffect5e} effect The effect.
* @param {Token5e} token The token.
* @returns {Promise<ActiveEffect5e>|false}
* @param {ActiveEffect5e} effect The effect.
* @param {Token5e} token The token.
* @param {object} [options]
* @param {ActiveEffect5e} [options.concentration] An optional concentration effect to act as the applied effect's
* origin instead.
* @returns {Promise<ActiveEffect5e|false>}
* @throws {Error} If the effect could not be applied.
* @protected
*/
static _applyEffectToToken(effect, token) {
if ( !game.user.isGM && !token.actor?.isOwner ) return false;
static async _applyEffectToToken(effect, token, { concentration }={}) {
const origin = concentration ?? effect;
if ( !game.user.isGM && !token.actor?.isOwner ) {
throw new Error(game.i18n.localize("DND5E.EffectApplyWarningOwnership"));
}

// Enable an existing effect on the target if it originated from this effect
const existingEffect = token.actor?.effects.find(e => e.origin === effect.uuid);
const existingEffect = token.actor?.effects.find(e => e.origin === origin.uuid);
if ( existingEffect ) {
return existingEffect.update({
...effect.constructor.getInitialDuration(),
disabled: false
});
}

if ( !game.user.isGM && concentration && !concentration.actor?.isOwner ) {
throw new Error(game.i18n.localize("DND5E.EffectApplyWarningConcentration"));
}

// Otherwise, create a new effect on the target
const effectData = foundry.utils.mergeObject(effect.toObject(), {
disabled: false,
transfer: false,
origin: effect.uuid
origin: origin.uuid
});
return ActiveEffect.implementation.create(effectData, { parent: token.actor });
const applied = await ActiveEffect.implementation.create(effectData, { parent: token.actor });
if ( concentration ) await concentration.addDependent(applied);
return applied;
}

/* -------------------------------------------- */
Expand Down

0 comments on commit 6bde97a

Please sign in to comment.