Skip to content

Commit

Permalink
Merge pull request #4836 from foundryvtt/npc-statblock-uses
Browse files Browse the repository at this point in the history
[#3798] Add limited uses labels for NPC stat block actions
  • Loading branch information
arbron authored Dec 11, 2024
2 parents cc61d75 + 3da7223 commit a4b26bf
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 a4b26bf

Please sign in to comment.