Skip to content

Commit

Permalink
[#3798] Add limited uses labels for NPC stat block actions
Browse files Browse the repository at this point in the history
During data preparation for the uses field generates a label that
matches the style of the NPC stat blocks, whihc is then used when
embedding them. This will use the item uses if available and fall
back to the activity uses otherwise. Any uses recovery that isn't
normally used for NPCs will not get a label.
  • Loading branch information
arbron committed Dec 11, 2024
1 parent cc61d75 commit 3da7223
Show file tree
Hide file tree
Showing 6 changed files with 48 additions and 9 deletions.
2 changes: 2 additions & 0 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion module/data/activity/base-activity.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
2 changes: 1 addition & 1 deletion module/data/activity/fields/consumption-targets-field.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
8 changes: 5 additions & 3 deletions module/data/actor/npc.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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*<p>/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) ) {
Expand Down
1 change: 1 addition & 0 deletions module/data/item/templates/activities.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
42 changes: 38 additions & 4 deletions module/data/shared/uses-field.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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={}) {
Expand Down Expand Up @@ -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 */
/* -------------------------------------------- */
Expand Down

0 comments on commit 3da7223

Please sign in to comment.