Skip to content

Commit

Permalink
Merge pull request #3219 from foundryvtt/summoning-compendiums
Browse files Browse the repository at this point in the history
[#893] Improve permissions checks & allow compendium summoning
  • Loading branch information
arbron authored Mar 18, 2024
2 parents a334960 + e985866 commit 991c00c
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 18 deletions.
14 changes: 11 additions & 3 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1481,9 +1481,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 @@ -1492,6 +1489,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 from a compendium.",
"NoActor": "Actor cannot be found with UUID '{uuid}' to summon.",
"NoOwnership": "You must have ownership of '{actor}' in order 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 Expand Up @@ -1841,6 +1845,10 @@
"Hint": "Disabling the token ring animations can improve performance."
},
"SETTINGS.DND5E": {
"ALLOWSUMMONING": {
"Name": "Allow Summoning",
"Hint": "Allow players to use summoning abilities to summon actors. Players must also have the Create Token core permission for this to work."
},
"THEME": {
"Name": "Theme",
"Hint": "Theme that will apply to the UI and all sheets by default. Automatic will be determined by your browser or operating system settings. Can be overridden on a sheet-by-sheet basis."
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 @@ -89,6 +89,20 @@ export class SummonsData extends foundry.abstract.DataModel {
/* Properties */
/* -------------------------------------------- */

/**
* Does the user have permissions to summon?
* @type {boolean}
*/
static get canSummon() {
return game.user.can("TOKEN_CREATE") && (game.user.isGM || game.settings.get("dnd5e", "allowSummoning"));
}

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

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

/**
* Item to which this summoning information belongs.
* @type {Item5e}
Expand All @@ -106,8 +120,12 @@ export class SummonsData extends foundry.abstract.DataModel {
* @param {string} profileId ID of the summoning profile to use.
*/
async summon(profileId) {
if ( !this.canSummon || !canvas.scene ) 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 @@ -120,9 +138,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 @@ -186,6 +207,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 @@ -345,8 +393,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
3 changes: 3 additions & 0 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 @@ -99,6 +100,8 @@ 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 ( 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";
const template = chatCard[0].querySelector('button[data-action="placeTemplate"]');
if ( template && !game.user.can("TEMPLATE_CREATE") ) template.style.display = "none";
return;
Expand Down
14 changes: 3 additions & 11 deletions module/documents/item.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1056,11 +1056,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 @@ -1116,7 +1112,7 @@ export default class Item5e extends SystemDocumentMixin(Item) {
if ( game.user.can("TEMPLATE_CREATE") && this.hasAreaTarget && canvas.scene ) {
config.createMeasuredTemplate = target.prompt;
}
if ( this.system.hasSummoning ) {
if ( this.system.hasSummoning && this.system.summons.canSummon && canvas.scene ) {
config.createSummons = summons.prompt;
config.summonsProfile = this.system.summons.profiles[0]._id;
}
Expand Down Expand Up @@ -2148,11 +2144,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
10 changes: 10 additions & 0 deletions module/settings.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,16 @@ export function registerSystemSettings() {
}
});

// Allow Summoning
game.settings.register("dnd5e", "allowSummoning", {
name: "SETTINGS.DND5E.ALLOWSUMMONING.Name",
hint: "SETTINGS.DND5E.ALLOWSUMMONING.Hint",
scope: "world",
config: true,
default: false,
type: Boolean
});

// Metric Unit Weights
game.settings.register("dnd5e", "metricWeightUnits", {
name: "SETTINGS.5eMetricN",
Expand Down

0 comments on commit 991c00c

Please sign in to comment.