Skip to content

Commit

Permalink
Merge pull request #3548 from trioderegion/feature/linked-summoning
Browse files Browse the repository at this point in the history
Summoning considerations for linked (e.g. unique) actors.
  • Loading branch information
arbron authored May 17, 2024
2 parents 4357865 + fe0b17e commit fedcc64
Showing 1 changed file with 60 additions and 13 deletions.
73 changes: 60 additions & 13 deletions module/data/item/fields/summons-field.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -278,21 +278,35 @@ export class SummonsData extends foundry.abstract.DataModel {
async fetchActor(uuid) {
const actor = await fromUuid(uuid);
if ( !actor ) throw new Error(game.i18n.format("DND5E.Summoning.Warning.NoActor", { uuid }));
if ( !actor.pack ) return actor;

// Search world actors to see if any have a matching summon ID flag
const actorLink = actor.prototypeToken.actorLink;
if ( !actor.pack && (!actorLink || actor.getFlag("dnd5e", "summon.origin") === this.item.uuid )) return actor;

// Search world actors to see if any usable summoned actor instances are present from prior summonings.
// Linked actors must match the summoning origin (item) to be considered.
const localActor = game.actors.find(a =>
a.getFlag("dnd5e", "summonedCopy") && (a.getFlag("core", "sourceId") === uuid)
// Has been cloned for summoning use
a.getFlag("dnd5e", "summonedCopy")
// Sourced from the desired actor UUID
&& (a.getFlag("core", "sourceId") === uuid)
// Unlinked or created from this item specifically
&& ((a.getFlag("dnd5e", "summon.origin") === this.item.uuid) || !a.prototypeToken.actorLink)
);
if ( localActor ) return localActor;

// Check permissions to create actors before importing
if ( !game.user.can("ACTOR_CREATE") ) throw new Error(game.i18n.localize("DND5E.Summoning.Warning.CreateActor"));

// Otherwise import the actor into the world and set the flag
return game.actors.importFromCompendium(game.packs.get(actor.pack), actor.id, {
"flags.dnd5e.summonedCopy": true
});
// No suitable world actor was found, create a new actor for this summoning instance.
if ( actor.pack ) {
// Template actor resides only in compendium, import the actor into the world and set the flag.
return game.actors.importFromCompendium(game.packs.get(actor.pack), actor.id, {
"flags.dnd5e.summonedCopy": true
});
} else {
// Template actor (linked) found in world, create a copy for this user's item.
return actor.clone({"flags.dnd5e.summonedCopy": true, "flags.core.sourceId": actor.uuid}, {save: true});
}
}

/* -------------------------------------------- */
Expand Down Expand Up @@ -381,23 +395,38 @@ export class SummonsData extends foundry.abstract.DataModel {
if ( this.bonuses.hp ) {
const hpBonus = new Roll(this.bonuses.hp, rollData);
await hpBonus.evaluate();

// If non-zero hp bonus, apply as needed for this actor.
// Note: Only unlinked actors will have their current HP set to their new max HP
if ( hpBonus.total ) {
if ( (actor.type === "pc") && !actor._source.system.attributes.hp.max ) {
actorUpdates.effects.push((new ActiveEffect({

// Helper function for modifying max HP ('bonuses.overall' or 'max')
const maxHpEffect = hpField => {
return (new ActiveEffect({
_id: staticID("dnd5eHPBonus"),
changes: [{
key: "system.attributes.hp.bonuses.overall",
key: `system.attributes.hp.${hpField}`,
mode: CONST.ACTIVE_EFFECT_MODES.ADD,
value: hpBonus.total
}],
disabled: false,
icon: "icons/magic/life/heart-glowing-red.webp",
name: game.i18n.localize("DND5E.Summoning.Bonuses.HitPoints.Label")
})).toObject());
})).toObject();
};

if ( !foundry.utils.isEmpty(actor.classes) && !actor._source.system.attributes.hp.max ) {
// Actor has classes without a hard-coded max -- apply bonuses to 'overall'
actorUpdates.effects.push(maxHpEffect("bonuses.overall"));
} else if ( actor.prototypeToken.actorLink ) {
// Otherwise, linked actors boost HP via 'max' AE
actorUpdates.effects.push(maxHpEffect("max"));
} else {
// Unlinked actors assumed to always be "fresh" copies with bonus HP added to both
// Max HP and Current HP
actorUpdates["system.attributes.hp.max"] = actor.system.attributes.hp.max + hpBonus.total;
actorUpdates["system.attributes.hp.value"] = actor.system.attributes.hp.value + hpBonus.total;
}
actorUpdates["system.attributes.hp.value"] = actor.system.attributes.hp.value + hpBonus.total;
}
}

Expand Down Expand Up @@ -519,7 +548,25 @@ export class SummonsData extends foundry.abstract.DataModel {

delete placement.prototypeToken;
const tokenDocument = await actor.getTokenDocument(foundry.utils.mergeObject(placement, tokenUpdates));
tokenDocument.delta.updateSource(actorUpdates);

// Linked summons require more explicit updates before token creation.
// Unlinked summons can take actor delta directly.
if ( tokenDocument.actorLink ) {
const { effects, items, ...rest } = actorUpdates;
await tokenDocument.actor.update(rest);
await tokenDocument.actor.updateEmbeddedDocuments("Item", items);

const { newEffects, oldEffects } = effects.reduce((acc, curr) => {
const target = tokenDocument.actor.effects.get(curr._id) ? "oldEffects" : "newEffects";
acc[target].push(curr);
return acc;
}, { newEffects: [], oldEffects: [] });

await tokenDocument.actor.updateEmbeddedDocuments("ActiveEffect", oldEffects);
await tokenDocument.actor.createEmbeddedDocuments("ActiveEffect", newEffects, {keepId: true});
} else {
tokenDocument.delta.updateSource(actorUpdates);
}

return tokenDocument.toObject();
}
Expand Down

0 comments on commit fedcc64

Please sign in to comment.