diff --git a/lang/en.json b/lang/en.json index 71113ac2db..042730abf6 100644 --- a/lang/en.json +++ b/lang/en.json @@ -2566,6 +2566,8 @@ "DND5E.ReactionAbbr": "R", "DND5E.ReactionPl": "Reactions", "DND5E.Recharge": "Recharge", +"DND5E.RechargeLong": "Recharge after a Long Rest", +"DND5E.RechargeShort": "Recharge after a Short or Long Rest", "DND5E.Recovery": "Recovery", "DND5E.RecoveryFormula": "Recovery Formula", "DND5E.RequiredMaterials": "Required Materials", diff --git a/module/data/activity/base-activity.mjs b/module/data/activity/base-activity.mjs index 74ec45c8f2..971387de98 100644 --- a/module/data/activity/base-activity.mjs +++ b/module/data/activity/base-activity.mjs @@ -46,7 +46,7 @@ const { * @property {TargetField} target * @property {boolean} target.override Override target values inferred from item. * @property {boolean} target.prompt Should the player be prompted to place the template? - * @property {UsesField} uses Uses available to this activity. + * @property {UsesData} uses Uses available to this activity. */ export default class BaseActivityData extends foundry.abstract.DataModel { diff --git a/module/data/activity/fields/consumption-targets-field.mjs b/module/data/activity/fields/consumption-targets-field.mjs index 7d9a09b4b5..c0e2de0adf 100644 --- a/module/data/activity/fields/consumption-targets-field.mjs +++ b/module/data/activity/fields/consumption-targets-field.mjs @@ -315,7 +315,7 @@ export class ConsumptionTargetData extends foundry.abstract.DataModel { * Calculate updates to activity or item uses. * @param {ActivityUseConfiguration} config Configuration data for the activity usage. * @param {object} options - * @param {UsesField} options.uses Uses data to consume. + * @param {UsesData} options.uses Uses data to consume. * @param {string} options.type Type label to be used in warning messages. * @param {BasicRoll[]} options.rolls Rolls performed as part of the usages. * @returns {{ spent: number, quantity: number }|null} diff --git a/module/data/actor/npc.mjs b/module/data/actor/npc.mjs index 4ac7f9e63b..ccda6a7b7e 100644 --- a/module/data/actor/npc.mjs +++ b/module/data/actor/npc.mjs @@ -576,13 +576,15 @@ export default class NPCData extends CreatureTemplate { for ( const item of this.parent.items ) { if ( !["feat", "weapon"].includes(item.type) ) continue; - const category = item.system.activities.contents[0]?.activation.type ?? "trait"; + const category = item.system.activities?.contents[0]?.activation.type ?? "trait"; if ( category in context.actionSections ) { const description = (await TextEditor.enrichHTML(item.system.description.value, { secrets: false, rollData: item.getRollData(), relativeTo: item })).replace(/^\s*
/g, "").replace(/<\/p>\s*$/g, ""); - // TODO: Add standard uses to item names (e.g. `Recharge 5-6`, `1/day`, etc.) - context.actionSections[category].actions.push({ name: item.name, description, sort: item.sort }); + const uses = item.system.uses.label || item.system.activities?.contents[0]?.uses.label; + context.actionSections[category].actions.push({ + name: uses ? `${item.name} (${uses})` : item.name, description, sort: item.sort + }); } } for ( const key of Object.keys(context.actionSections) ) { diff --git a/module/data/item/templates/activities.mjs b/module/data/item/templates/activities.mjs index c0898a501a..a5ddc4c355 100644 --- a/module/data/item/templates/activities.mjs +++ b/module/data/item/templates/activities.mjs @@ -7,6 +7,7 @@ import UsesField from "../../shared/uses-field.mjs"; * Data model template for items with activities. * * @property {ActivityCollection} activities Activities on this item. + * @property {UsesData} uses Item's limited uses & recovery. * @mixin */ export default class ActivitiesTemplate extends SystemDataModel { diff --git a/module/data/shared/uses-field.mjs b/module/data/shared/uses-field.mjs index 078613a061..1b36e6cb98 100644 --- a/module/data/shared/uses-field.mjs +++ b/module/data/shared/uses-field.mjs @@ -3,6 +3,13 @@ import FormulaField from "../fields/formula-field.mjs"; const { ArrayField, NumberField, SchemaField, StringField } = foundry.data.fields; +/** + * @typedef {object} UsesData + * @property {number} spent Number of uses that have been spent. + * @property {string} max Formula for the maximum number of uses. + * @property {UsesRecoveryData[]} recovery Recovery profiles for this activity's uses. + */ + /** * Data for a recovery profile for an activity's uses. * @@ -14,10 +21,6 @@ const { ArrayField, NumberField, SchemaField, StringField } = foundry.data.field /** * Field for storing uses data. - * - * @property {number} spent Number of uses that have been spent. - * @property {string} max Formula for the maximum number of uses. - * @property {UsesRecoveryData[]} recovery Recovery profiles for this activity's uses. */ export default class UsesField extends SchemaField { constructor(fields={}, options={}) { @@ -72,12 +75,43 @@ export default class UsesField extends SchemaField { } if ( labels ) labels.recovery = game.i18n.getListFormatter({ style: "narrow" }).format(periods); + this.uses.label = UsesField.getStatblockLabel.call(this); + Object.defineProperty(this.uses, "rollRecharge", { value: UsesField.rollRecharge.bind(this.parent?.system ? this.parent : this), configurable: true }); } + /* -------------------------------------------- */ + + /** + * Create a label for uses data that matches the style seen on NPC stat blocks. Complex recovery data might result + * in no label being generated if it doesn't represent recovery that can be normally found on a NPC. + * @this {ItemDataModel|BaseActivityData} + * @returns {string} + */ + static getStatblockLabel() { + if ( !this.uses.max || (this.uses.recovery.length !== 1) ) return ""; + const recovery = this.uses.recovery[0]; + + // Recharge X–Y + if ( recovery.period === "recharge" ) { + const value = parseInt(recovery.formula); + return `${game.i18n.localize("DND5E.Recharge")} ${value === 6 ? "6" : `${value}–6`}`; + } + + // Recharge after a Short or Long Rest + if ( ["lr", "sr"].includes(recovery.period) && (this.uses.max === 1) ) { + return game.i18n.localize(`DND5E.Recharge${recovery.period === "sr" ? "Short" : "Long"}`); + } + + // X/Day + const period = CONFIG.DND5E.limitedUsePeriods[recovery.period === "sr" ? "sr" : "day"]?.label ?? ""; + if ( !period ) return ""; + return `${this.uses.max}/${period}`; + } + /* -------------------------------------------- */ /* Helpers */ /* -------------------------------------------- */