diff --git a/module/applications/activity/activity-sheet.mjs b/module/applications/activity/activity-sheet.mjs index 4f353f25d7..43a9bd17f6 100644 --- a/module/applications/activity/activity-sheet.mjs +++ b/module/applications/activity/activity-sheet.mjs @@ -204,8 +204,7 @@ export default class ActivitySheet extends Application5e { }; // Consumption targets - const canScale = this.activity.consumption.scaling.allowed - || (this.item.type === "spell" && this.item.system.level !== 0); + const canScale = this.activity.canScale; const consumptionTypeOptions = Array.from(this.activity.validConsumptionTypes).map(value => ({ value, label: CONFIG.DND5E.activityConsumptionTypes[value].label diff --git a/module/applications/activity/activity-usage-dialog.mjs b/module/applications/activity/activity-usage-dialog.mjs new file mode 100644 index 0000000000..89e88f8655 --- /dev/null +++ b/module/applications/activity/activity-usage-dialog.mjs @@ -0,0 +1,26 @@ +import Application5e from "../api/application.mjs"; + +/** + * Dialog for configuring the usage of an activity. + */ +export default class ActivityUsageDialog extends Application5e { + + // TODO: Implement activation dialog + + /* -------------------------------------------- */ + /* Factory Methods */ + /* -------------------------------------------- */ + + /** + * Display the activity usage dialog. + * @param {Activity} activity Activity to use. + * @param {ActivityUseConfiguration} config Configuration data for the usage. + * @param {object} options Additional options for the application. + * @returns {Promise} Form data object with results of the activation. + */ + static async create(activity, config, options) { + return new Promise(resolve => { + resolve({}); + }); + } +} diff --git a/module/data/item/feat.mjs b/module/data/item/feat.mjs index 87588117bf..d66e4a0ced 100644 --- a/module/data/item/feat.mjs +++ b/module/data/item/feat.mjs @@ -106,7 +106,8 @@ export default class FeatData extends ItemDataModel.mixin( { since: "DnD5e 4.0", until: "DnD5e 4.4" } ); return uses.period === "recharge" ? Number(uses.formula) : null; - } + }, + configurable: true }); Object.defineProperty(this.recharge, "charged", { get() { @@ -116,7 +117,8 @@ export default class FeatData extends ItemDataModel.mixin( { since: "DnD5e 4.0", until: "DnD5e 4.4" } ); return uses.value > 0; - } + }, + configurable: true }); } diff --git a/module/documents/activity/mixin.mjs b/module/documents/activity/mixin.mjs index 804638fd99..623af7b885 100644 --- a/module/documents/activity/mixin.mjs +++ b/module/documents/activity/mixin.mjs @@ -1,3 +1,4 @@ +import ActivityUsageDialog from "../../applications/activity/activity-usage-dialog.mjs"; import PseudoDocumentMixin from "../mixins/pseudo-document.mjs"; /** @@ -10,9 +11,12 @@ export default Base => class extends PseudoDocumentMixin(Base) { * Configuration information for Activities. * * @typedef {PseudoDocumentsMetadata} ActivityMetadata - * @property {string} type Type name of this activity. - * @property {string} img Default icon. - * @property {string} title Default title. + * @property {string} type Type name of this activity. + * @property {string} img Default icon. + * @property {string} title Default title. + * @property {object} usage + * @property {string} usage.chatCard Template used to render the chat card. + * @property {typeof ActivityUsageDialog} usage.dialog Default usage prompt. */ /** @@ -20,13 +24,27 @@ export default Base => class extends PseudoDocumentMixin(Base) { * @type {PseudoDocumentsMetadata} */ static metadata = Object.freeze({ - name: "Activity" + name: "Activity", + usage: { + chatCard: "systems/dnd5e/templates/chat/activity-card.hbs", + dialog: ActivityUsageDialog + } }); /* -------------------------------------------- */ /* Properties */ /* -------------------------------------------- */ + /** + * Is scaling possible with this activity? + * @type {boolean} + */ + get canScale() { + return this.consumption.scaling.allowed || (this.isSpell && this.item.system.level > 0); + } + + /* -------------------------------------------- */ + /** * Is this activity on a spell? * @type {boolean} @@ -37,6 +55,18 @@ export default Base => class extends PseudoDocumentMixin(Base) { /* -------------------------------------------- */ + /** + * Does activating this activity consume a spell slot? + * @type {boolean} + */ + get requiresSpellSlot() { + if ( !this.isSpell || !this.actor?.system.spells ) return false; + // TODO: Check against specific preparation modes here + return this.item.system.level > 0; + } + + /* -------------------------------------------- */ + /** * Consumption targets that can be use for this activity. * @type {Set} @@ -47,6 +77,560 @@ export default Base => class extends PseudoDocumentMixin(Base) { return types; } + /* -------------------------------------------- */ + /* Activation */ + /* -------------------------------------------- */ + + /** + * Configuration data for an activity usage being prepared. + * + * @typedef {object} ActivityUseConfiguration + * @property {object|false} create + * @property {boolean} create.measuredTemplate Should this item create a template? + * @property {object} concentration + * @property {boolean} concentration.begin Should this usage initiate concentration? + * @property {string|null} concentration.end ID of an active effect to end concentration on. + * @property {object|false} consume + * @property {boolean|string[]} consume.resources Set to `true` or `false` to enable or disable all resource + * consumption or provide a list of consumption type keys defined in + * `CONFIG.DND5E.activityConsumptionTypes` to only enable those types. + * @property {boolean} consume.spellSlot Should this spell consume a spell slot? + * @property {Event} event The browser event which triggered the item usage, if any. + * @property {boolean|number} scaling Number of steps above baseline to scale this usage, or `false` if + * scaling is not allowed. + * @property {object} spell + * @property {number} spell.slot The spell slot to consume. + */ + + /** + * Data for the activity activation configuration dialog. + * + * @typedef {object} ActivityDialogConfiguration + * @property {boolean} [configure=true] Display a configuration dialog for the item usage, if applicable? + * @property {typeof ActivityActivationDialog} [applicationClass] Alternate activation dialog to use. + * @property {object} [options] Options passed through to the dialog. + */ + + /** + * Message configuration for activity usage. + * + * @typedef {object} ActivityMessageConfiguration + * @property {boolean} [create=true] Whether to automatically create a chat message (if true) or simply return + * the prepared chat message data (if false). + * @property {object} [data={}] Additional data used when creating the message. + * @property {string} [rollMode] The roll display mode with which to display (or not) the card. + */ + + /** + * Details of final changes performed by the usage. + * + * @typedef {object} ActivityUsageResults + * @property {ActiveEffect5e[]} effects Active effects that were created or deleted. + * @property {ChatMessage5e|object} message The chat message created for the activation, or the message data + * if `create` in ActivityMessageConfiguration was `false`. + * @property {MeasuredTemplateDocument[]} templates Created measured templates. + * @property {ActivityUsageUpdates} updates Updates to the actor & items. + */ + + /** + * Activate this activity. + * @param {ActivityUseConfiguration} usage Configuration info for the activation. + * @param {ActivityDialogConfiguration} dialog Configuration info for the usage dialog. + * @param {ActivityMessageConfiguration} message Configuration info for the created chat message. + * @returns {Promise} Details on the usage process if not canceled. + */ + async use(usage={}, dialog={}, message={}) { + if ( !this.item.isOwner ) { + ui.notifications.error("DND5E.DocumentUseWarn", { localize: true }); + return; + } + + // Create an item clone to work with throughout the rest of the process + let item = this.item.clone({}, { keepId: true }); + item.prepareData(); + item.prepareFinalAttributes(); + let activity = item.system.activities.get(this.id); + + const usageConfig = activity._prepareUsageConfig(usage); + + const dialogConfig = foundry.utils.mergeObject({ + configure: true, + applicationClass: this.metadata.usage.dialog + }, dialog); + + const messageConfig = foundry.utils.mergeObject({ + create: true, + data: { + flags: { + dnd5e: { + activity: { type: this.type, id: this.id, uuid: this.uuid }, + item: { type: this.item.type, id: this.item.id, uuid: this.item.uuid } + } + } + } + }, message); + + /** + * A hook event that fires before an activity usage is configured. + * @function dnd5e.preUseActivity + * @memberof hookEvents + * @param {Activity} activity Activity being used. + * @param {ActivityUseConfiguration} usageConfig Configuration info for the activation. + * @param {ActivityDialogConfiguration} dialogConfig Configuration info for the usage dialog. + * @param {ActivityMessageConfiguration} messageConfig Configuration info for the created chat message. + * @returns {boolean} Explicitly return `false` to prevent activity from being used. + */ + if ( Hooks.call("dnd5e.preUseActivity", activity, usageConfig, dialogConfig, messageConfig) === false ) return; + + if ( "dnd5e.preUseItem" in Hooks.events ) { + foundry.utils.logCompatibilityWarning( + "The `dnd5e.preUseItem` hook has been deprecated and replaced with `dnd5e.preUseItem`.", + { since: "DnD5e 4.0", until: "DnD5e 4.4" } + ); + const { config, options } = this._createDeprecatedConfigs(usageConfig, dialogConfig, messageConfig); + if ( Hooks.call("dnd5e.preUseItem", item, config, options) === false ) return; + this._applyDeprecatedConfigs(usageConfig, dialogConfig, messageConfig, config, options); + } + + // Display configuration window if necessary + if ( dialogConfig.configure && activity._requiresConfigurationDialog(usageConfig) ) { + try { + await dialogConfig.applicationClass.create(activity, usageConfig, dialogConfig.options); + } catch(err) { + return; + } + } + + // Handle scaling + activity._prepareUsageScaling(usageConfig, messageConfig, item); + activity = item.system.activities.get(this.id); + + // Handle consumption + const updates = await activity.consume(usageConfig, messageConfig); + if ( updates === false ) return; + const results = { effects: [], templates: [], updates }; + + // Create concentration effect & end previous effects + if ( usageConfig.concentration?.begin ) { + const effect = await item.actor.beginConcentrating(item); + if ( effect ) { + results.effects ??= []; + results.effects.push(effect); + foundry.utils.setProperty(messageConfig.data, "flags.dnd5e.use.concentrationId", effect.id); + } + if ( usageConfig.concentration?.end ) { + const deleted = await item.actor.endConcentration(usageConfig.concentration.end); + results.effects.push(...deleted); + } + } + + // Create chat message + messageConfig.data.rolls = (messageConfig.data.rolls ?? []).concat(updates.rolls); + results.message = await activity._createUsageMessage(messageConfig); + + // Perform any final usage steps + await activity._finalizeUsage(usageConfig, results); + + /** + * A hook event that fires when an activity is activated. + * @function dnd5e.postUseActivity + * @memberof hookEvents + * @param {Activity} activity Activity being activated. + * @param {ActivityUseConfiguration} usageConfig Configuration data for the activation. + * @param {ActivityUsageResults} results Final details on the activation. + */ + Hooks.callAll("dnd5e.postUseActivity", activity, usageConfig, results); + + if ( "dnd5e.useItem" in Hooks.events ) { + foundry.utils.logCompatibilityWarning( + "The `dnd5e.useItem` hook has been deprecated and replaced with `dnd5e.postUseActivity`.", + { since: "DnD5e 4.0", until: "DnD5e 4.4" } + ); + const { config, options } = this._createDeprecatedConfigs(usageConfig, dialogConfig, messageConfig); + Hooks.callAll("dnd5e.itemUsageConsumption", item, config, options, results.templates, results.effects, null); + } + + return results; + } + + /* -------------------------------------------- */ + + /** + * Consume this activation's usage. + * @param {ActivityUseConfiguration} usageConfig Usage configuration. + * @param {ActivityMessageConfiguration} messageConfig Configuration data for the chat message. + * @returns {ActivityUsageUpdates|false} + */ + async consume(usageConfig, messageConfig) { + /** + * A hook event that fires before an item's resource consumption is calculated. + * @function dnd5e.preActivityConsumption + * @memberof hookEvents + * @param {Activity} activity Activity being activated. + * @param {ActivityUseConfiguration} usageConfig Configuration data for the activation. + * @param {ActivityMessageConfiguration} messageConfig Configuration info for the created chat message. + * @returns {boolean} Explicitly return `false` to prevent activity from being activated. + */ + if ( Hooks.call("dnd5e.preActivityConsumption", this, usageConfig, messageConfig) === false ) return; + + if ( "dnd5e.preItemUsageConsumption" in Hooks.events ) { + foundry.utils.logCompatibilityWarning( + "The `dnd5e.preItemUsageConsumption` hook has been deprecated and replaced with `dnd5e.preActivityConsumption`.", + { since: "DnD5e 4.0", until: "DnD5e 4.4" } + ); + const { config, options } = this._createDeprecatedConfigs(usageConfig, {}, messageConfig); + if ( Hooks.call("dnd5e.preItemUsageConsumption", item, config, options) === false ) return; + this._applyDeprecatedConfigs(usageConfig, {}, messageConfig, config, options); + } + + const updates = await this._prepareUsageUpdates(usageConfig); + if ( !updates ) return false; + + foundry.utils.setProperty(messageConfig, "data.flags.dnd5e.use.consumed", usageConfig.consume); + + /** + * A hook event that fires after an item's resource consumption is calculated, but before any updates are performed. + * @function dnd5e.activityConsumption + * @memberof hookEvents + * @param {Activity} activity Activity being activated. + * @param {ActivityUseConfiguration} usageConfig Configuration data for the activation. + * @param {ActivityMessageConfiguration} messageConfig Configuration info for the created chat message. + * @param {ActivityUsageUpdates} updates Updates to apply to the actor and other documents. + * @returns {boolean} Explicitly return `false` to prevent activity from being activated. + */ + if ( Hooks.call("dnd5e.activityConsumption", this, usageConfig, messageConfig, updates) === false ) return; + + if ( "dnd5e.itemUsageConsumption" in Hooks.events ) { + foundry.utils.logCompatibilityWarning( + "The `dnd5e.itemUsageConsumption` hook has been deprecated and replaced with `dnd5e.activityConsumption`.", + { since: "DnD5e 4.0", until: "DnD5e 4.4" } + ); + const { config, options } = this._createDeprecatedConfigs(usageConfig, {}, messageConfig); + const usage = { + actorUpdates: updates.actor, + deleteIds: updates.delete, + itemUpdates: updates.item.find(i => i._id === this.item.id), + resourceUpdates: updates.item.filter(i => i._id !== this.item.id) + }; + if ( Hooks.call("dnd5e.itemUsageConsumption", item, config, options, usage) === false ) return; + this._applyDeprecatedConfigs(usageConfig, {}, messageConfig, config, options); + updates.actor = usage.actorUpdates; + updates.delete = usage.deleteIds; + updates.item = usage.resourceUpdates; + if ( !foundry.utils.isEmpty(usage.itemUpdates) ) updates.item.push({ _id: this.item.id, ...usage.itemUpdates }); + } + + // Merge activity changes into the item updates + if ( !foundry.utils.isEmpty(updates.activity) ) { + const itemIndex = updates.item.findIndex(i => i._id === this.item.id); + const keyPath = `system.activities.${this.id}`; + const activityUpdates = foundry.utils.expandObject(updates.activity); + if ( itemIndex === -1 ) updates.item.push({ _id: this.item.id, [keyPath]: activityUpdates }); + else updates.item[itemIndex][keyPath] = activityUpdates; + } + + // Update documents with consumption + if ( !foundry.utils.isEmpty(updates.actor) ) await this.actor.update(updates.actor); + if ( !foundry.utils.isEmpty(updates.delete) ) await this.actor.deleteEmbeddedDocuments("Item", updates.delete); + if ( !foundry.utils.isEmpty(updates.item) ) await this.actor.updateEmbeddedDocuments("Item", updates.item); + + /** + * A hook event that fires after an item's resource consumption is calculated and applied. + * @function dnd5e.postActivityConsumption + * @memberof hookEvents + * @param {Activity} activity Activity being activated. + * @param {ActivityUseConfiguration} usageConfig Configuration data for the activation. + * @param {ActivityMessageConfiguration} messageConfig Configuration info for the created chat message. + * @param {ActivityUsageUpdates} updates Applied updates to the actor and other documents. + * @returns {boolean} Explicitly return `false` to prevent activity from being activated. + */ + if ( Hooks.call("dnd5e.postActivityConsumption", this, usageConfig, messageConfig, updates) === false ) return; + + return updates; + } + + /* -------------------------------------------- */ + + /** + * Translate new config objects back into old config objects for deprecated hooks. + * @param {ActivityUseConfiguration} usageConfig + * @param {ActivityDialogConfiguration} dialogConfig + * @param {ActivityMessageConfiguration} messageConfig + * @returns {{ config: ItemUseConfiguration, options: ItemUseOptions }} + * @internal + */ + _createDeprecatedConfigs(usageConfig, dialogConfig, messageConfig) { + return { + config: { + createMeasuredTemplate: usageConfig.create?.measuredTemplate ?? null, + consumeResource: usageConfig.consume?.resources !== false ?? null, + consumeSpellSlot: usageConfig.consume?.spellSlot !== false ?? null, + consumeUsage: (usageConfig.consume?.resources.includes("itemUses") + || usageConfig.consume?.resources.includes("activityUses")) ?? null, + slotLevel: usageConfig.spell?.slot ?? null, + resourceAmount: usageConfig.scaling ?? null, + beginConcentrating: usageConfig.concentration?.begin ?? false, + endConcentration: usageConfig.concentration?.end ?? null + }, + options: { + configureDialog: dialogConfig.configure, + rollMode: messageConfig.rollMode, + createMessage: messageConfig.create, + flags: messageConfig.data?.flags, + event: usageConfig.event + } + }; + } + + /* -------------------------------------------- */ + + /** + * Apply changes from old config objects back onto new config objects. + * @param {ActivityUseConfiguration} usageConfig + * @param {ActivityDialogConfiguration} dialogConfig + * @param {ActivityMessageConfiguration} messageConfig + * @param {ItemUseConfiguration} config + * @param {ItemUseOptions} options + * @internal + */ + _applyDeprecatedConfigs(usageConfig, dialogConfig, messageConfig, config, options) { + const usageTypes = ["activityUses", "itemUses"]; + let resources; + if ( config.consumeResource && config.consumeUsage ) resources = true; + else if ( config.consumeResource && (config.consumeUsage === false) ) { + resources = Array.from(Object.keys(CONFIG.DND5E.activityConsumptionTypes)).filter(k => !usageTypes.includes(k)); + } + else if ( (config.consumeResource === false) && config.consumeUsage ) resources = usageTypes; + + foundry.utils.mergeObject(usageConfig, { + create: { + measuredTemplate: config.createMeasuredTemplate + }, + concentration: { + begin: config.beginConcentrating, + end: config.endConcentration + }, + consume: { + resources, + spellSlot: config.consumeSpellSlot + }, + scaling: config.resourceAmount, + spell: { + slot: config.slotLevel + } + }); + foundry.utils.mergeObject(dialogConfig, { + configure: options.configureDialog + }); + foundry.utils.mergeObject(messageConfig, { + create: options.createMessage, + rollMode: options.rollMode, + data: { + flags: options.flags + } + }); + } + + /* -------------------------------------------- */ + + /** + * Prepare usage configuration with the necessary defaults. + * @param {ActivityUseConfiguration} config Configuration object passed to the `use` method. + * @returns {ActivityUseConfiguration} + * @protected + */ + _prepareUsageConfig(config) { + config = foundry.utils.deepClone(config); + + if ( config.create !== false ) { + config.create ??= {}; + config.create.measuredTemplate ??= !!this.target.template.type; + // TODO: Re-implement `system.target.prompt` from item data + // TODO: Handle permissions checks in `ActivityUsageDialog` + } + + if ( config.consume !== false ) { + config.consume ??= {}; + config.consume.resources ??= this.consumption.targets.length > 0; + config.consume.spellSlot ??= this.requiresSpellSlot; + } + + if ( this.canScale ) config.scaling ??= 0; + else config.scaling = false; + + if ( this.isSpell ) { + const mode = this.item.system.preparation.mode; + config.spell ??= {}; + config.spell.slot ??= (mode in this.actor.system.spells) ? mode : `spell${this.item.system.level}`; + } + + if ( this.item.requiresConcentration && !game.settings.get("dnd5e", "disableConcentration") ) { + config.concentration ??= {}; + config.concentration.begin ??= true; + const { effects } = this.actor.concentration; + const limit = this.actor.system.attributes?.concentration?.limit ?? 0; + if ( limit && (limit <= effects.size) ) config.concentration.end = effects.find(e => { + const data = e.flags.dnd5e?.itemData ?? {}; + return (data === this.id) || (data._id === this.id); + })?.id ?? effects.first()?.id ?? null; + } + + return config; + } + + /* -------------------------------------------- */ + + /** + * Determine scaling values and update item clone if necessary. + * @param {ActivityUseConfiguration} usageConfig Configuration data for the activation. + * @param {ActivityMessageConfiguration} messageConfig Configuration data for the chat message. + * @param {Item5e} item Clone of the item that contains this activity. + * @protected + */ + _prepareUsageScaling(usageConfig, messageConfig, item) { + // TODO: Implement scaling + } + + /* -------------------------------------------- */ + + /** + * Update data produced by activity usage. + * + * @typedef {object} ActivityUsageUpdates + * @property {object} activity Updates applied to activity that performed the activation. + * @property {object} actor Updates applied to the actor that performed the activation. + * @property {string[]} delete IDs of items to be deleted from the actor. + * @property {object[]} item Updates applied to items on the actor that performed the activation. + * @property {Roll[]} rolls Any rolls performed as part of the activation. + */ + + /** + * Calculate changes to actor, items, & this activity based on resource consumption. + * @param {ActivityUseConfiguration} config Usage configuration. + * @returns {ActivityUsageUpdates} + * @protected + */ + async _prepareUsageUpdates(config) { + const updates = { activity: {}, actor: {}, delete: [], item: [], rolls: [] }; + // TODO: Handle consumption + return updates; + } + + /* -------------------------------------------- */ + + /** + * Determine if the configuration dialog is required based on the configuration options. Does not guarantee a dialog + * is shown if the dialog is suppressed in the activation dialog configuration. + * @param {ActivityUseConfiguration} config + * @returns {boolean} + * @protected + */ + _requiresConfigurationDialog(config) { + const checkObject = obj => (foundry.utils.getType(obj) === "Object") && Object.values(obj).some(v => v); + return config.concentration?.begin === true + || checkObject(config.create) + || checkObject(config.consume) + || (config.scaling !== false); + } + + /* -------------------------------------------- */ + + /** + * Prepare the context used to render the usage chat card. + * @returns {object} + * @protected + */ + async _usageChatContext() { + const data = await this.item.system.getCardData(); + const properties = [...(data.tags ?? []), ...(data.properties ?? [])]; + const supplements = []; + if ( (this.activation.type === "reaction") && this.activation.condition ) { + supplements.push(`${game.i18n.localize("DND5E.Reaction")} ${this.activation.condition}`); + } + if ( data.materials?.value ) { + supplements.push(`${game.i18n.localize("DND5E.Materials")} ${data.materials.value}`); + } + return { + activity: this, + actor: this.item.actor, + item: this.item, + token: this.item.actor?.token, + buttons: null, + description: data.description.chat, + properties: properties.length ? properties : null, + subtitle: data.subtitle ?? this.item.system.chatFlavor, + supplements + }; + } + + /* -------------------------------------------- */ + + /** + * Display a chat message for this usage. + * @param {ActivityMessageConfiguration} message Configuration info for the created message. + * @returns {Promise} + * @protected + */ + async _createUsageMessage(message) { + const context = await this._usageChatContext(); + const messageConfig = foundry.utils.mergeObject({ + rollMode: game.settings.get("core", "rollMode"), + data: { + content: await renderTemplate(this.metadata.usage.chatCard, context), + speaker: ChatMessage.getSpeaker({ actor: this.item.actor }), + flags: { + core: { canPopout: true } + } + } + }, message); + + /** + * A hook event that fires before an activity usage card is created. + * @function dnd5e.preCreateUsageMessage + * @memberof hookEvents + * @param {Activity} activity Activity for which the card will be created. + * @param {ActivityMessageConfiguration} message Configuration info for the created message. + */ + Hooks.callAll("dnd5e.preCreateUsageMessage", this, messageConfig); + + ChatMessage.applyRollMode(messageConfig.data, messageConfig.rollMode); + const card = messageConfig.create === false ? messageConfig.data : await ChatMessage.create(messageConfig.data); + + /** + * A hook event that fires after an activity usage card is created. + * @function dnd5e.postCreateUsageMessage + * @memberof hookEvents + * @param {Activity} activity Activity for which the card was created. + * @param {ChatMessage5e|object} card Created card or configuration data if not created. + */ + Hooks.callAll("dnd5e.postCreateUsageMessage", this, card); + + return card; + } + + /* -------------------------------------------- */ + + /** + * Perform any final steps of the activation including creating measured templates. + * @param {ActivityUseConfiguration} config Configuration data for the activation. + * @param {ActivityUsageResults} results Final details on the activation. + * @protected + */ + async _finalizeUsage(config, results) { + results.templates = []; + if ( config.create?.measuredTemplate ) { + try { + results.templates = await (dnd5e.canvas.AbilityTemplate.fromItem(this.item))?.drawPreview(); + } catch(err) { + Hooks.onError("Activity#use", err, { + msg: game.i18n.localize("DND5E.PlaceTemplateError"), + log: "error", + notify: "error" + }); + } + } + } + /* -------------------------------------------- */ /* Helpers */ /* -------------------------------------------- */ diff --git a/module/documents/item.mjs b/module/documents/item.mjs index d788d6a6e7..d631ee699f 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -903,52 +903,27 @@ export default class Item5e extends SystemDocumentMixin(Item) { * @param {ItemUseOptions} [options] Options used for configuring item usage. * @returns {Promise} Chat message if options.createMessage is true, message data if it is * false, and nothing if the roll wasn't performed. + * @deprecated since DnD5e 4.0, targeted for removal in DnD5e 4.4 */ async use(config={}, options={}) { - if ( !this.isOwner ) { - ui.notifications.error("DND5E.DocumentUseWarn", { localize: true }); - return null; - } - let item = this; - const is = item.system; - const as = item.actor.system; - - // Ensure the options object is ready - options = foundry.utils.mergeObject({ - configureDialog: true, - createMessage: true, - "flags.dnd5e.use": {type: this.type, itemId: this.id, itemUuid: this.uuid} - }, options); - - // Define follow-up actions resulting from the item usage - if ( config.consumeSlotLevel ) { - console.warn("You are passing 'consumeSlotLevel' to the ItemUseConfiguration object, which now expects a key as 'slotLevel'."); - config.slotLevel = config.consumeSlotLevel; - delete config.consumeSlotLevel; - } - config = foundry.utils.mergeObject(this._getUsageConfig(), config); - - /** - * A hook event that fires before an item usage is configured. - * @function dnd5e.preUseItem - * @memberof hookEvents - * @param {Item5e} item Item being used. - * @param {ItemUseConfiguration} config Configuration data for the item usage being prepared. - * @param {ItemUseOptions} options Additional options used for configuring item usage. - * @returns {boolean} Explicitly return `false` to prevent item from being used. - */ - if ( Hooks.call("dnd5e.preUseItem", item, config, options) === false ) return; - - // Are any default values necessitating a prompt? - const needsConfiguration = Object.values(config).includes(true); - - // Display configuration dialog - if ( (options.configureDialog !== false) && needsConfiguration ) { - const configuration = await AbilityUseDialog.create(item, config); - if ( !configuration ) return; - foundry.utils.mergeObject(config, configuration); + foundry.utils.logCompatibilityWarning( + "The `Item5e#use` method has been deprecated and should now be called directly on the activity to be used.", + { since: "DnD5e 4.0", until: "DnD5e 4.4" } + ); + if ( this.system.activities ) { + const activity = this.system.activities.contents[0]; + if ( activity ) { + const usageConfig = {}; + const dialogConfig = {}; + const messageConfig = {}; + activity._applyDeprecatedConfigs(usageConfig, dialogConfig, messageConfig, config, options); + return activity.use(usageConfig, dialogConfig, messageConfig); + } } + if ( this.actor ) return this.displayCard(); + else return; + // TODO: Remove this reference code once EnchantmentActivity and SummoningActivity are created and scaling is added // Store selected enchantment profile in flag if ( config.enchantmentProfile ) { foundry.utils.setProperty(options.flags, "dnd5e.use.enchantmentProfile", config.enchantmentProfile); @@ -981,37 +956,6 @@ export default class Item5e extends SystemDocumentMixin(Item) { // Calculate and consume item consumption if ( await this.consume(item, config, options) === false ) return; - // Initiate or end concentration. - const effects = []; - if ( config.beginConcentrating ) { - const effect = await item.actor.beginConcentrating(item); - 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 ) { - try { - templates = await (dnd5e.canvas.AbilityTemplate.fromItem(item))?.drawPreview(); - } catch(err) { - Hooks.onError("Item5e#use", err, { - msg: game.i18n.localize("DND5E.PlaceTemplateError"), - log: "error", - notify: "error" - }); - } - } - // Initiate summons creation let summoned; if ( config.createSummons ) { @@ -1021,21 +965,6 @@ export default class Item5e extends SystemDocumentMixin(Item) { Hooks.onError("Item5e#use", err, { log: "error", notify: "error" }); } } - - /** - * A hook event that fires when an item is used, after the measured template has been created if one is needed. - * @function dnd5e.useItem - * @memberof hookEvents - * @param {Item5e} item Item being used. - * @param {ItemUseConfiguration} config Configuration data for the roll. - * @param {ItemUseOptions} options Additional options for configuring item usage. - * @param {MeasuredTemplateDocument[]|null} templates The measured templates if they were created. - * @param {ActiveEffect5e[]} effects The active effects that were created or deleted. - * @param {TokenDocument5e[]|null} summoned Summoned tokens if they were created. - */ - Hooks.callAll("dnd5e.useItem", item, config, options, templates ?? null, effects, summoned ?? null); - - return cardData; } /* -------------------------------------------- */ @@ -1046,117 +975,44 @@ export default class Item5e extends SystemDocumentMixin(Item) { * @param {ItemUseConfiguration} config Configuration data for the item usage being prepared. * @param {ItemUseOptions} options Additional options used for configuring item usage. * @returns {false|void} Returns `false` if any further usage should be canceled. + * @deprecated since DnD5e 4.0, targeted for removal in DnD5e 4.4 */ async consume(item, config, options) { - /** - * A hook event that fires before an item's resource consumption has been calculated. - * @function dnd5e.preItemUsageConsumption - * @memberof hookEvents - * @param {Item5e} item Item being used. - * @param {ItemUseConfiguration} config Configuration data for the item usage being prepared. - * @param {ItemUseOptions} options Additional options used for configuring item usage. - * @returns {boolean} Explicitly return `false` to prevent item from being used. - */ - if ( Hooks.call("dnd5e.preItemUsageConsumption", item, config, options) === false ) return false; - - // Determine whether the item can be used by testing the chosen values of the config. - const usage = item._getUsageUpdates(config); - if ( !usage ) return false; - - options.flags ??= {}; - if ( config.consumeUsage ) foundry.utils.setProperty(options.flags, "dnd5e.use.consumedUsage", true); - if ( config.consumeResource ) foundry.utils.setProperty(options.flags, "dnd5e.use.consumedResource", true); - if ( config.consumeSpellSlot ) foundry.utils.setProperty(options.flags, "dnd5e.use.consumedSpellSlot", true); - - /** - * A hook event that fires after an item's resource consumption has been calculated but before any - * changes have been made. - * @function dnd5e.itemUsageConsumption - * @memberof hookEvents - * @param {Item5e} item Item being used. - * @param {ItemUseConfiguration} config Configuration data for the item usage being prepared. - * @param {ItemUseOptions} options Additional options used for configuring item usage. - * @param {object} usage - * @param {object} usage.actorUpdates Updates that will be applied to the actor. - * @param {object} usage.itemUpdates Updates that will be applied to the item being used. - * @param {object[]} usage.resourceUpdates Updates that will be applied to other items on the actor. - * @param {Set} usage.deleteIds Item ids for those which consumption will delete. - * @returns {boolean} Explicitly return `false` to prevent item from being used. - */ - if ( Hooks.call("dnd5e.itemUsageConsumption", item, config, options, usage) === false ) return false; - - // Commit pending data updates - const { actorUpdates, itemUpdates, resourceUpdates, deleteIds } = usage; - if ( !foundry.utils.isEmpty(itemUpdates) ) await item.update(itemUpdates); - if ( !foundry.utils.isEmpty(deleteIds) ) await this.actor.deleteEmbeddedDocuments("Item", [...deleteIds]); - if ( !foundry.utils.isEmpty(actorUpdates) ) await this.actor.update(actorUpdates); - if ( !foundry.utils.isEmpty(resourceUpdates) ) await this.actor.updateEmbeddedDocuments("Item", resourceUpdates); + foundry.utils.logCompatibilityWarning( + "The `Item5e#consume` method has been deprecated and should now be called directly on the activity.", + { since: "DnD5e 4.0", until: "DnD5e 4.4" } + ); + if ( this.system.activities ) { + const activity = this.system.activities.contents[0]; + if ( activity ) { + const usageConfig = {}; + const dialogConfig = {}; + const messageConfig = {}; + activity._applyDeprecatedConfigs(usageConfig, dialogConfig, messageConfig, config, options); + return activity.consume(usageConfig, messageConfig); + } + } + return false; } /* -------------------------------------------- */ - /** - * Prepare an object of possible and default values for item usage. A value that is `null` is ignored entirely. - * @returns {ItemUseConfiguration} Configuration data for the roll. - */ + /** @deprecated */ _getUsageConfig() { - const { consume, uses, summons, target, level, preparation } = this.system; - - const config = { - createMeasuredTemplate: null, - createSummons: null, - consumeResource: null, - consumeSpellSlot: null, - consumeUsage: null, - enchantmentProfile: null, - promptEnchantment: null, - slotLevel: null, - summonsProfile: null, - resourceAmount: null, - beginConcentrating: null, - endConcentration: null - }; + return; + + // TODO: Remove this reference code once EnchantmentActivity & SummoningActivity are created - const scaling = this.usageScaling; - if ( scaling === "slot" ) { - const spells = this.actor.system.spells ?? {}; - config.consumeSpellSlot = true; - config.slotLevel = (preparation?.mode in spells) ? preparation.mode : `spell${level}`; - } else if ( scaling === "resource" ) { - config.resourceAmount = consume.amount || 1; - } - if ( this.hasLimitedUses ) config.consumeUsage = uses.prompt; - if ( this.hasResource ) { - config.consumeResource = true; - // Do not suggest consuming your own uses if also consuming them through resources. - if ( consume.target === this.id ) config.consumeUsage = null; - } - if ( game.user.can("TEMPLATE_CREATE") && this.hasAreaTarget && canvas.scene ) { - config.createMeasuredTemplate = target.prompt; - } if ( this.system.isEnchantment ) { const availableEnchantments = EnchantmentData.availableEnchantments(this); config.promptEnchantment = availableEnchantments.length > 1; config.enchantmentProfile = availableEnchantments[0]?.id; } + if ( this.system.hasSummoning && this.system.summons.canSummon && canvas.scene ) { config.createSummons = summons.prompt; config.summonsProfile = this.system.summons.profiles[0]._id; } - if ( this.requiresConcentration && !game.settings.get("dnd5e", "disableConcentration") ) { - config.beginConcentrating = true; - const { effects } = this.actor.concentration; - const limit = this.actor.system.attributes?.concentration?.limit ?? 0; - if ( limit && (limit <= effects.size) ) { - const id = effects.find(e => { - const data = e.flags.dnd5e?.itemData ?? {}; - return (data === this.id) || (data._id === this.id); - })?.id ?? effects.first()?.id ?? null; - config.endConcentration = id; - } - } - - return config; } /* -------------------------------------------- */ diff --git a/templates/activity/parts/activity-consumption.hbs b/templates/activity/parts/activity-consumption.hbs index 65a8c13a37..6918084468 100644 --- a/templates/activity/parts/activity-consumption.hbs +++ b/templates/activity/parts/activity-consumption.hbs @@ -29,6 +29,7 @@ + {{#unless activity.isSpell}} {{#with fields.consumption.fields.scaling.fields as |fields|}}
{{ localize "DND5E.ACTIVITY.FIELDS.consumption.scaling.label" }} @@ -38,6 +39,7 @@ {{/if}}
{{/with}} + {{/unless}} {{> "systems/dnd5e/templates/shared/uses-values.hbs" }} {{> "systems/dnd5e/templates/shared/uses-recovery.hbs" }} diff --git a/templates/chat/activity-card.hbs b/templates/chat/activity-card.hbs new file mode 100644 index 0000000000..1a3f3b9e2f --- /dev/null +++ b/templates/chat/activity-card.hbs @@ -0,0 +1,66 @@ +
+
+
+ {{ item.name }} +
+ {{ item.name }} + {{#if subtitle}} + {{{ subtitle }}} + {{/if}} +
+ +
+
+
{{{ description }}}
+
+
+ + {{#if buttons}} +
+ {{#each buttons}} + + {{/each}} +
+ {{/if}} + + {{#each supplements}} +

{{{ this }}}

+ {{/each}} + + {{#if properties}} + + {{/if}} + + {{!-- TODO + + --}} +
diff --git a/templates/chat/item-card.hbs b/templates/chat/item-card.hbs index 00747f5b8a..e289b0e00c 100644 --- a/templates/chat/item-card.hbs +++ b/templates/chat/item-card.hbs @@ -27,115 +27,6 @@ - {{!-- Item Actions --}} - {{#if hasButtons}} -
- - {{!-- Attacks --}} - {{#if hasAttack}} - - {{/if}} - - {{!-- Damage / Healing --}} - {{#if hasDamage}} - - {{/if}} - - {{!-- Versatile --}} - {{#if isVersatile}} - - {{/if}} - - {{!-- Saving Throw --}} - {{#if hasSave}} - {{#with item.system.save}} - - {{/with}} - {{/if}} - - {{!-- Other Formula --}} - {{#if item.system.formula}} - - {{/if}} - - {{!-- Templates --}} - {{#if hasAreaTarget}} - - {{/if}} - - {{!-- Summoning --}} - {{#if item.system.hasSummoning}} - - {{/if}} - - {{!-- Tool Check --}} - {{#if isTool}} - - {{/if}} - - {{!-- Ability Check --}} - {{#if hasAbilityCheck}} - - {{/if}} - - {{!-- Consume Use --}} - {{#if consumeUsage}} - - {{/if}} - - {{!-- Consume Resource --}} - {{#if consumeResource}} - - {{/if}} -
- {{/if}} - - {{!-- Reactions --}} - {{#if (and data.activation.condition (eq data.activation.type "reaction"))}} -

{{ localize "DND5E.Reaction" }}{{ data.activation.condition }}

- {{/if}} - {{!-- Materials --}} {{#if data.materials.value}}

{{ localize "DND5E.Materials" }}{{ data.materials.value }}

@@ -156,30 +47,4 @@ {{/each}} {{/if}} - - {{!-- Applicable Effects --}} -