Skip to content

Commit

Permalink
[foundryvtt#1964, foundryvtt#2349] Allow placing templates from activ…
Browse files Browse the repository at this point in the history
…ities

Adds a new `AbilityTemplate#fromActivity` method and deprecates
the old `fromItem` method. This new method supports placing
multiple templates at a time.

Also switches to using `getSnappedPoint` rather than the
deprecated `getSnappedPosition` which has the added benefit of
working properly on hex grids.

Closes foundryvtt#2349
  • Loading branch information
arbron committed Aug 2, 2024
1 parent 65b1d55 commit cd499e6
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 41 deletions.
106 changes: 75 additions & 31 deletions module/canvas/ability-template.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,34 @@ export default class AbilityTemplate extends MeasuredTemplate {
/* -------------------------------------------- */

/**
* A factory method to create an AbilityTemplate instance using provided data from an Item5e instance
* @param {Item5e} item The Item object for which to construct the template
* A factory method to create an AbilityTemplate instance using provided data from an Activity instance.
* @param {Activity} activity The Activity for which to construct the template.
* @param {object} [options={}] Options to modify the created template.
* @returns {AbilityTemplate|null} The template object, or null if the item does not produce a template
* @returns {AbilityTemplate[]|null} The template objects, or null if the item does not produce a template.
*/
static fromItem(item, options={}) {
const target = item.system.target ?? {};
static fromActivity(activity, options={}) {
const target = activity.target?.template ?? {};
const templateShape = dnd5e.config.areaTargetTypes[target.type]?.template;
if ( !templateShape ) return null;

// Prepare template data
const templateData = foundry.utils.mergeObject({
t: templateShape,
user: game.user.id,
distance: target.value,
distance: target.size,
direction: 0,
x: 0,
y: 0,
fillColor: game.user.color,
flags: { dnd5e: { origin: item.uuid, spellLevel: item.system.level } }
flags: { dnd5e: {
dimensions: {
size: target.size,
width: target.width,
height: target.height
},
origin: activity.uuid,
spellLevel: activity.item.system.level
} }
}, options);

// Additional type-specific data
Expand All @@ -56,9 +64,9 @@ export default class AbilityTemplate extends MeasuredTemplate {
templateData.angle = CONFIG.MeasuredTemplate.defaults.angle;
break;
case "rect": // 5e rectangular AoEs are always cubes
templateData.width = target.value;
templateData.width = target.size;
if ( game.settings.get("dnd5e", "gridAlignedSquareTemplates") ) {
templateData.distance = Math.hypot(target.value, target.value);
templateData.distance = Math.hypot(target.size, target.size);
templateData.direction = 45;
} else {
// Override as 'ray' to make the template able to be rotated without morphing its shape
Expand All @@ -73,32 +81,71 @@ export default class AbilityTemplate extends MeasuredTemplate {
}

/**
* A hook event that fires before a template is created for an Item.
* @function dnd5e.preCreateItemTemplate
* A hook event that fires before a template is created for an Activity.
* @function dnd5e.preCreateActivityTemplate
* @memberof hookEvents
* @param {Item5e} item Item for which the template is being placed.
* @param {object} templateData Data used to create the new template.
* @returns {boolean} Explicitly return false to prevent the template from being placed.
* @param {Activity} activity Activity for which the template is being placed.
* @param {object} templateData Data used to create the new template.
* @returns {boolean} Explicitly return `false` to prevent the template from being placed.
*/
if ( Hooks.call("dnd5e.preCreateItemTemplate", item, templateData) === false ) return null;
if ( Hooks.call("dnd5e.preCreateActivityTemplate", activity, templateData) === false ) return null;

if ( "dnd5e.preCreateItemTemplate" in Hooks.events ) {
foundry.utils.logCompatibilityWarning(
"The `dnd5e.preCreateItemTemplate` hook has been deprecated and replaced with `dnd5e.preCreateActivityTemplate`.",
{ since: "DnD5e 4.0", until: "DnD5e 4.4" }
);
if ( Hooks.call("dnd5e.preCreateItemTemplate", activity.item, templateData) === false ) return null;
}

// Return the template constructed from the item data
// Construct the templates from activity data
const cls = CONFIG.MeasuredTemplate.documentClass;
const template = new cls(templateData, {parent: canvas.scene});
const object = new this(template);
object.item = item;
object.actorSheet = item.actor?.sheet || null;
const created = Array.fromRange(target.count || 1).map(() => {
const template = new cls(foundry.utils.deepClone(templateData), { parent: canvas.scene });
const object = new this(template);
object.activity = activity;
object.item = activity.item;
object.actorSheet = activity.actor?.sheet || null;
return object;
});

/**
* A hook event that fires after a template is created for an Item.
* @function dnd5e.createItemTemplate
* A hook event that fires after a template are created for an Activity.
* @function dnd5e.createActivityTemplate
* @memberof hookEvents
* @param {Item5e} item Item for which the template is being placed.
* @param {AbilityTemplate} template The template being placed.
* @param {Activity} activity Activity for which the template is being placed.
* @param {AbilityTemplate[]} templates The templates being placed.
*/
Hooks.callAll("dnd5e.createItemTemplate", item, object);
Hooks.callAll("dnd5e.createActivityTemplate", activity, created);

if ( "dnd5e.createItemTemplate" in Hooks.events ) {
foundry.utils.logCompatibilityWarning(
"The `dnd5e.createItemTemplate` hook has been deprecated and replaced with `dnd5e.createActivityTemplate`.",
{ since: "DnD5e 4.0", until: "DnD5e 4.4" }
);
Hooks.callAll("dnd5e.createItemTemplate", activity.item, created[0]);
}

return created;
}

return object;
/* -------------------------------------------- */

/**
* A factory method to create an AbilityTemplate instance using provided data from an Item5e instance
* @param {Item5e} item The Item object for which to construct the template
* @param {object} [options={}] Options to modify the created template.
* @returns {AbilityTemplate|null} The template object, or null if the item does not produce a template
* @deprecated since DnD5e 4.0, available until DnD5e 4.4
*/
static fromItem(item, options={}) {
foundry.utils.logCompatibilityWarning(
"The `AbilityTemplate#fromItem` method has been deprecated and replaced with `fromActivity`.",
{ since: "DnD5e 4.0", until: "DnD5e 4.4" }
);
const activity = this.system.activities?.contents[0];
if ( activity ) return this.fromActivity(activity, options)?.[0] ?? null;
return null;
}

/* -------------------------------------------- */
Expand Down Expand Up @@ -176,9 +223,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
const now = Date.now(); // Apply a 20ms throttle
if ( now - this.#moveTime <= 20 ) return;
const center = event.data.getLocalPosition(this.layer);
const interval = canvas.grid.type === CONST.GRID_TYPES.GRIDLESS ? 0 : 2;
const snapped = canvas.grid.getSnappedPosition(center.x, center.y, interval);
this.document.updateSource({x: snapped.x, y: snapped.y});
this.document.updateSource(canvas.templates.getSnappedPoint(center));
this.refresh();
this.#moveTime = now;
}
Expand Down Expand Up @@ -207,8 +252,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
*/
async _onConfirmPlacement(event) {
await this._finishPlacement(event);
const interval = canvas.grid.type === CONST.GRID_TYPES.GRIDLESS ? 0 : 2;
const destination = canvas.grid.getSnappedPosition(this.document.x, this.document.y, interval);
const destination = canvas.templates.getSnappedPoint({ x: this.document.x, y: this.document.y });
this.document.updateSource(destination);
this.#events.resolve(canvas.scene.createEmbeddedDocuments("MeasuredTemplate", [this.document.toObject()]));
}
Expand Down
5 changes: 4 additions & 1 deletion module/documents/activity/mixin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,10 @@ export default Base => class extends PseudoDocumentMixin(Base) {
results.templates = [];
if ( config.create?.measuredTemplate ) {
try {
results.templates = await (dnd5e.canvas.AbilityTemplate.fromItem(this.item))?.drawPreview();
for ( const template of dnd5e.canvas.AbilityTemplate.fromActivity(this) ) {
const result = await template.drawPreview();
if ( result ) results.templates.push(result);
}
} catch(err) {
Hooks.onError("Activity#use", err, {
msg: game.i18n.localize("DND5E.PlaceTemplateError"),
Expand Down
9 changes: 0 additions & 9 deletions module/documents/mixins/pseudo-document.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,6 @@ export default Base => class extends Base {
*/
static _sheets = new Map();

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

/** @inheritDoc */
_initialize(options) {
super._initialize(options);
if ( !game._documentsReady ) return;
return this.prepareData();
}

/* -------------------------------------------- */
/* Model Configuration */
/* -------------------------------------------- */
Expand Down

0 comments on commit cd499e6

Please sign in to comment.