diff --git a/lang/en.json b/lang/en.json
index 644ea5fa76..503d178ce3 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -156,8 +156,10 @@
},
"DND5E.ACTIVITY": {
- "Label": "Activity",
- "LabelPl": "Activities",
+ "Title": {
+ "one": "Activity",
+ "other": "Activities"
+ },
"FIELDS": {
"activation": {
"label": "Activation",
@@ -207,6 +209,9 @@
"hint": "Value of the duration in the specified units, if applicable."
}
},
+ "effects": {
+ "label": "Applied Effects"
+ },
"img": {
"label": "Icon"
},
@@ -740,7 +745,7 @@
"DND5E.ConsumableWithoutCharges": "available units to use",
"DND5E.Consumption": {
"Action": {
- "Add": "Add Consumption Target",
+ "Create": "Create Consumption Target",
"Delete": "Delete Consumption Target"
},
"Scaling": {
@@ -943,6 +948,12 @@
"DND5E.Dusk": "Dusk",
"DND5E.Effect": "Effect",
"DND5E.Effects": "Effects",
+"DND5E.EFFECT": {
+ "Action": {
+ "Create": "Create Effect",
+ "Delete": "Delete Effect"
+ }
+},
"DND5E.EffectsApplyTokens": "Apply to selected tokens",
"DND5E.EffectApplyWarningConcentration": "Applying an effect that is being concentrated on by another character requires GM permissions.",
"DND5E.EffectApplyWarningOwnership": "Effects cannot be applied to tokens you are not the owner of.",
@@ -2137,7 +2148,7 @@
},
"Recovery": {
"Action": {
- "Add": "Add Recovery Profile",
+ "Create": "Create Recovery Profile",
"Delete": "Delete Recovery Profile"
},
"Recharge": {
diff --git a/module/applications/activity/activity-sheet.mjs b/module/applications/activity/activity-sheet.mjs
index a163bb20f3..4f353f25d7 100644
--- a/module/applications/activity/activity-sheet.mjs
+++ b/module/applications/activity/activity-sheet.mjs
@@ -24,8 +24,10 @@ export default class ActivitySheet extends Application5e {
},
actions: {
addConsumption: ActivitySheet.#addConsumption,
+ addEffect: ActivitySheet.#addEffect,
addRecovery: ActivitySheet.#addRecovery,
deleteConsumption: ActivitySheet.#deleteConsumption,
+ deleteEffect: ActivitySheet.#deleteEffect,
deleteRecovery: ActivitySheet.#deleteRecovery
},
form: {
@@ -59,7 +61,10 @@ export default class ActivitySheet extends Application5e {
]
},
effect: {
- template: "systems/dnd5e/templates/activity/effect.hbs"
+ template: "systems/dnd5e/templates/activity/effect.hbs",
+ templates: [
+ "systems/dnd5e/templates/activity/parts/activity-effects.hbs"
+ ]
}
};
@@ -277,6 +282,12 @@ export default class ActivitySheet extends Application5e {
*/
async _prepareEffectContext(context) {
context.tab = context.tabs.effect;
+
+ const appliedEffects = new Set(context.activity.effects.map(e => e.id));
+ context.allEffects = this.item.effects.map(effect => ({
+ value: effect.id, label: effect.name, selected: appliedEffects.has(effect.id)
+ }));
+
return context;
}
@@ -358,7 +369,7 @@ export default class ActivitySheet extends Application5e {
/** @override */
_canRender(options) {
if ( !this.isVisible ) throw new Error(game.i18n.format("SHEETS.DocumentSheetPrivate", {
- type: game.i18n.localize("DND5E.ACTIVITY.Label")
+ type: game.i18n.localize("DND5E.ACTIVITY.Title.one")
}));
}
@@ -377,6 +388,15 @@ export default class ActivitySheet extends Application5e {
this.activity.constructor._unregisterApp(this.activity, this);
}
+ /* -------------------------------------------- */
+
+ /** @inheritDoc */
+ async _renderFrame(options) {
+ const frame = await super._renderFrame(options);
+ frame.autocomplete = "off";
+ return frame;
+ }
+
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
@@ -401,6 +421,25 @@ export default class ActivitySheet extends Application5e {
/* -------------------------------------------- */
+ /**
+ * Handle creating a new active effect and adding it to the applied effects list.
+ * @this {ActivityConfig}
+ * @param {Event} event Triggering click event.
+ * @param {HTMLElement} target Button that was clicked.
+ */
+ static async #addEffect(event, target) {
+ const effectData = {
+ name: this.item.name,
+ img: this.item.img,
+ origin: this.item.uuid,
+ transfer: false
+ };
+ const [created] = await this.item.createEmbeddedDocuments("ActiveEffect", [effectData]);
+ this.activity.update({ effects: [...this.activity.toObject().effects, { id: created.id }] });
+ }
+
+ /* -------------------------------------------- */
+
/**
* Handle adding a new entry to the uses recovery list.
* @this {ActivityConfig}
@@ -437,6 +476,23 @@ export default class ActivitySheet extends Application5e {
/* -------------------------------------------- */
+ /**
+ * Handle deleting an active effect and removing it from the applied effects list.
+ * @this {ActivityConfig}
+ * @param {Event} event Triggering click event.
+ * @param {HTMLElement} target Button that was clicked.
+ */
+ static async #deleteEffect(event, target) {
+ const effectId = target.closest("[data-effect-id]")?.dataset.effectId;
+ const result = await this.item.effects.get(effectId)?.deleteDialog();
+ if ( result instanceof ActiveEffect ) {
+ const effects = this.activity.toObject().effects.filter(e => e.id !== effectId);
+ this.activity.update({ effects });
+ }
+ }
+
+ /* -------------------------------------------- */
+
/**
* Handle removing an entry from the uses recovery list.
* @this {ActivityConfig}
@@ -474,6 +530,14 @@ export default class ActivitySheet extends Application5e {
*/
_prepareSubmitData(event, formData) {
const submitData = foundry.utils.expandObject(formData.object);
+ if ( foundry.utils.hasProperty(submitData, "appliedEffects") ) {
+ const effects = submitData.effects ?? this.activity.toObject().effects;
+ submitData.effects = effects.filter(e => submitData.appliedEffects.includes(e.id));
+ for ( const id of submitData.appliedEffects ) {
+ if ( submitData.effects.find(e => e.id === id) ) continue;
+ submitData.effects.push({ id });
+ }
+ }
if ( foundry.utils.hasProperty(submitData, "consumption.targets") ) {
submitData.consumption.targets = Object.values(submitData.consumption.targets);
}
diff --git a/module/data/activity/base-activity.mjs b/module/data/activity/base-activity.mjs
index cf41016b1e..195d695a58 100644
--- a/module/data/activity/base-activity.mjs
+++ b/module/data/activity/base-activity.mjs
@@ -19,6 +19,13 @@ const {
* @property {string} scaling.formula Specific scaling formula if not automatically calculated from target's value.
*/
+/**
+ * Data for effects that can be applied.
+ *
+ * @typedef {object} EffectApplicationData
+ * @property {string} effect ID of the effect to apply.
+ */
+
/**
* Data for a recovery profile for an activity's uses.
*
@@ -48,6 +55,7 @@ const {
* @property {string} duration.value Scalar value for the activity's duration.
* @property {string} duration.units Units that are used for the duration.
* @property {string} duration.special Description of any special duration details.
+ * @property {EffectApplicationData[]} effects Linked effects that can be applied.
* @property {object} range
* @property {string} range.value Scalar value for the activity's range.
* @property {string} range.units Units that are used for the range.
@@ -120,6 +128,9 @@ export default class BaseActivityData extends foundry.abstract.DataModel {
units: new StringField({ initial: "inst" }),
special: new StringField()
}),
+ effects: new ArrayField(new SchemaField({
+ id: new DocumentIdField()
+ })),
range: new SchemaField({
value: new FormulaField({ deterministic: true }),
units: new StringField(),
@@ -156,6 +167,11 @@ export default class BaseActivityData extends foundry.abstract.DataModel {
prepareData() {
this.name = this.name || game.i18n.localize(this.metadata?.title);
this.img = this.img || this.metadata?.img;
+ const item = this.item;
+ this.effects.forEach(e => Object.defineProperty(e, "effect", {
+ get() { return item.effects.get(e.id); },
+ configurable: true
+ }));
UsesField.prepareData.call(this, this.getRollData({ deterministic: true }));
}
diff --git a/module/documents/mixins/pseudo-document.mjs b/module/documents/mixins/pseudo-document.mjs
index 0dbef4877b..0155f71aef 100644
--- a/module/documents/mixins/pseudo-document.mjs
+++ b/module/documents/mixins/pseudo-document.mjs
@@ -159,7 +159,9 @@ export default Base => class extends Base {
* @param {RenderOptions} [options] Rendering options.
*/
render(options) {
- for ( const app of this.constructor._apps.get(this.uuid) ?? [] ) app.render(options);
+ for ( const app of this.constructor._apps.get(this.uuid) ?? [] ) {
+ app.render({ window: { title: app.title }, ...options });
+ }
}
/* -------------------------------------------- */
diff --git a/templates/activity/effect.hbs b/templates/activity/effect.hbs
index 66f7d81821..f50c8b0240 100644
--- a/templates/activity/effect.hbs
+++ b/templates/activity/effect.hbs
@@ -1,3 +1,3 @@