Skip to content

Commit

Permalink
[#893] Improve permissions checks & allow compendium summoning
Browse files Browse the repository at this point in the history
Adds a `SummonsData#canSummon` property that determines whether a
scene is open and whether the user has permissions to summon. If not,
then no summoning control is displayed in the `AbilityUseDialog` and
the chat button is removed.

If user is trying to summon and actor that they do not have ownership
of the will be shown an error.

Also introduces a check for file browser permission when creating
token data if the token is set to use a wildcard image. If the user
lacks permission here, then the token will fall back to using the
portrait image and a warning will be displayed in the console.

Adds a new `SummonsData#fetchActor` data which handles locating the
actor to be summoned. If the actor is in the world, then it just
returns that version. If the associated actor is in a compendium, it
will first search through world actors to find one with a matching
`sourceId` and the `dnd5e.summonedCopy` flag set, summoning from the
pre-imported version if available. Only then will it fall back on
importing the actor from the compendium so long as the user has
permission to do so.
  • Loading branch information
arbron committed Mar 13, 2024
1 parent 816902c commit 15450f0
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 20 deletions.
10 changes: 7 additions & 3 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1424,9 +1424,6 @@
"Hint": "Modify to saving throw DCs on the summoned creature's abilities to match that of the summoner."
}
},
"Placement": {
"GenericError": "Failed to place summoned actor"
},
"Profile": {
"Label": "Summons Profile",
"LabelPl": "Summons Profiles",
Expand All @@ -1435,6 +1432,13 @@
"Prompt": {
"Label": "Summon Prompt",
"Hint": "Disable the summoning prompt when item is used. Players will still be able to summon from the chat card."
},
"Warning": {
"CreateActor": "You must have 'Create New Actors' permission in order to summon directly a compendium.",
"NoActor": "Actor cannot be found with UUID '{uuid}' to summon.",
"NoOwnership": "You do not have ownership of '{actor}' which you must have to summon it.",
"NoProfile": "Cannot find summoning profile {profileId} on '{item}'.",
"Wildcard": "You must have 'Use File Browser' permissions to summon creatures with wildcard artwork."
}
},
"DND5E.Supply": "Supply",
Expand Down
64 changes: 61 additions & 3 deletions module/data/item/fields/summons-field.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,20 @@ export class SummonsData extends foundry.abstract.DataModel {
/* Properties */
/* -------------------------------------------- */

/**
* Is a scene present and does the user have permissions to summon?
* @type {boolean}
*/
static get canSummon() {
return canvas.scene && game.user.can("TOKEN_CREATE");
}

get canSummon() {
return this.constructor.canSummon;
}

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

/**
* Item to which this summoning information belongs.
* @type {Item5e}
Expand All @@ -90,8 +104,12 @@ export class SummonsData extends foundry.abstract.DataModel {
* @param {string} profileId ID of the summoning profile to use.
*/
async summon(profileId) {
if ( !this.canSummon ) return;

const profile = this.profiles.find(p => p._id === profileId);
if ( !profile ) throw new Error(`Cannot find summoning profile ${profileId} on ${this.item.id}.`);
if ( !profile ) {
throw new Error(game.i18n.format("DND5E.Summoning.Warning.NoProfile", { profileId, item: this.item.name }));
}

/**
* A hook event that fires before summoning is performed.
Expand All @@ -104,9 +122,12 @@ export class SummonsData extends foundry.abstract.DataModel {
if ( Hooks.call("dnd5e.preSummon", this.item, profile) === false ) return;

// Fetch the actor that will be summoned
let actor = await fromUuid(profile.uuid);
const actor = await this.fetchActor(profile.uuid);

// TODO: Import actor into world if inside compendium
// Verify ownership of actor
if ( !actor.isOwner ) {
throw new Error(game.i18n.format("DND5E.Summoning.Warning.NoOwnership", { actor: actor.name }));
}

const tokensData = [];
const minimized = !this.item.parent?.sheet._minimized;
Expand Down Expand Up @@ -170,6 +191,33 @@ export class SummonsData extends foundry.abstract.DataModel {

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

/**
* If actor to be summoned is in a compendium, create a local copy or use an already imported version if present.
* @param {string} uuid UUID of actor that will be summoned.
* @returns {Actor5e} Local copy of actor.
*/
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 localActor = game.actors.find(a =>
a.getFlag("dnd5e", "summonedCopy") && (a.getFlag("core", "sourceId") === uuid)
);
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
});
}

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

/**
* Prepare the updates to apply to the summoned actor.
* @param {Actor5e} actor Actor that will be modified.
Expand Down Expand Up @@ -314,8 +362,18 @@ export class SummonsData extends foundry.abstract.DataModel {
* @returns {object}
*/
async getTokenData({ actor, placement, tokenUpdates, actorUpdates }) {
if ( actor.prototypeToken.randomImg && !game.user.can("FILES_BROWSE") ) {
tokenUpdates.texture ??= {};
tokenUpdates.texture.src ??= actor.img;
ui.notifications.warn("DND5E.Summoning.Warning.Wildcard", { localize: true });
}

const tokenDocument = await actor.getTokenDocument(foundry.utils.mergeObject(placement, tokenUpdates));
tokenDocument.delta.updateSource(actorUpdates);
tokenDocument.updateSource({
"flags.dnd5e.summon": { origin: this.item.uuid }
});

return tokenDocument.toObject();
}
}
2 changes: 1 addition & 1 deletion module/data/item/templates/action.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export default class ActionTemplate extends ItemDataModel {
* @type {boolean}
*/
get hasSummoning() {
return (this.actionType === "summ") && !!this.summons.profiles.length;
return (this.actionType === "summ") && !!this.summons?.profiles.length;
}

/* -------------------------------------------- */
Expand Down
2 changes: 2 additions & 0 deletions module/data/token/token-system-flags.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const {
* @property {boolean} isPolymorphed Is the actor represented by this token transformed?
* @property {string} originalActor Original actor before transformation.
* @property {object} previousActorData Actor data from before transformation for unlinked tokens.
* @property {object} summon Data for summoned tokens.
* @property {TokenRingFlagData} tokenRing
*/
export default class TokenSystemFlags extends foundry.abstract.DataModel {
Expand All @@ -33,6 +34,7 @@ export default class TokenSystemFlags extends foundry.abstract.DataModel {
required: false, initial: undefined, idOnly: true
}),
previousActorData: new ObjectField({required: false, initial: undefined}),
summon: new ObjectField({required: false, initial: undefined}),
tokenRing: new SchemaField({
enabled: new BooleanField({label: "DND5E.TokenRings.Enabled"}),
colors: new SchemaField({
Expand Down
8 changes: 6 additions & 2 deletions module/documents/chat-message.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SummonsData } from "../data/item/fields/summons-field.mjs";
import simplifyRollFormula from "../dice/simplify-roll-formula.mjs";
import DamageRoll from "../dice/damage-roll.mjs";

Expand Down Expand Up @@ -91,8 +92,11 @@ export default class ChatMessage5e extends ChatMessage {

// If the user is the message author or the actor owner, proceed
let actor = game.actors.get(this.speaker.actor);
if ( actor && actor.isOwner ) return;
else if ( game.user.isGM || (this.user.id === game.user.id)) return;
if ( game.user.isGM || actor?.isOwner || (this.user.id === game.user.id) ) {
const summonsButton = chatCard[0].querySelector('button[data-action="summon"]');
if ( summonsButton && !SummonsData.canSummon ) summonsButton.style.display = "none";
return;
}

// Otherwise conceal action buttons except for saving throw
const buttons = chatCard.find("button[data-action]:not(.apply-effect)");
Expand Down
14 changes: 3 additions & 11 deletions module/documents/item.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1040,11 +1040,7 @@ export default class Item5e extends SystemDocumentMixin(Item) {
try {
summoned = await item.system.summons.summon(config.summonsProfile);
} catch(err) {
Hooks.onError("Item5e#use", err, {
msg: game.i18n.localize("DND5E.Summoning.Placement.GenericError"),
log: "error",
notify: "error"
});
Hooks.onError("Item5e#use", err, { log: "error", notify: "error" });
}
}

Expand Down Expand Up @@ -1095,7 +1091,7 @@ export default class Item5e extends SystemDocumentMixin(Item) {
if ( consume.target === this.id ) config.consumeUsage = null;
}
if ( game.user.can("TEMPLATE_CREATE") && this.hasAreaTarget ) config.createMeasuredTemplate = target.prompt;
if ( this.system.hasSummoning ) {
if ( this.system.hasSummoning && this.system.summons.canSummon ) {
config.createSummons = summons.prompt;
config.summonsProfile = this.system.summons.profiles[0]._id;
}
Expand Down Expand Up @@ -2093,11 +2089,7 @@ export default class Item5e extends SystemDocumentMixin(Item) {
try {
await item.system.summons.summon(summonsProfile);
} catch(err) {
Hooks.onError("Item5e#_onChatCardSummon", err, {
msg: game.i18n.localize("DND5E.Summoning.Placement.GenericError"),
log: "error",
notify: "error"
});
Hooks.onError("Item5e#_onChatCardSummon", err, { log: "error", notify: "error" });
}
}

Expand Down

0 comments on commit 15450f0

Please sign in to comment.