Skip to content

Commit 0dae221

Browse files
committed
[#5559, #6722] Add Activity Behaviors that attach to templates
Adds a new system for registering Activity Behaviors. These will be automatically attached to any measured templates created from that activity. An Activity Behavior represents a simplified data model for configuring a Region Behavior, allowing for certain parts of the Region Behavior's configuration to be automatically populated based on activity settings (for example, the Difficult Terrain behavior gets its "Magical" property set automatically based on whether the item has the magical property set). Since players cannot create region behaviors themselves, the creation of these behaviors is handled by the active GM user after the player places the template in the scene. Closes #6722
1 parent d735ee1 commit 0dae221

28 files changed

Lines changed: 540 additions & 56 deletions

dnd5e.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,7 @@ Hooks.once("i18nInit", () => {
534534
}
535535
utils.performPreLocalization(CONFIG.DND5E);
536536
Object.values(CONFIG.DND5E.activityTypes).forEach(c => c.documentClass.localize());
537+
Object.values(CONFIG.DND5E.activityBehaviorTypes).forEach(c => c.model.localize());
537538
Object.values(CONFIG.DND5E.advancementTypes).forEach(c => c.documentClass.localize());
538539
foundry.helpers.Localization.localizeDataModel(dataModels.settings.CalendarConfigSetting);
539540
foundry.helpers.Localization.localizeDataModel(dataModels.settings.CalendarPreferencesSetting);
@@ -634,6 +635,8 @@ Hooks.on("chatMessage", (app, message, data) => applications.Award.chatMessage(m
634635
Hooks.on("createChatMessage", dataModels.chatMessage.RequestMessageData.onCreateMessage);
635636
Hooks.on("updateChatMessage", dataModels.chatMessage.RequestMessageData.onUpdateResultMessage);
636637

638+
Hooks.on("createRegion", documents.activity.UtilityActivity.placeTemplateBehaviors);
639+
637640
Hooks.on("renderActorDirectory", (app, html, data) => documents.Actor5e.onRenderActorDirectory(html));
638641

639642
Hooks.on("getActorContextOptions", documents.Actor5e.addDirectoryContextOptions);

icons/LICENSE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ The dnd5e system for Foundry Virtual Tabletop includes icon artwork licensed fro
4646
/svg/actors/group.svg - "Tattered banner" by Lorc under CC BY 3.0
4747
/svg/actors/npc.svg - "Spiked dragon head" by Delapouite under CC BY 3.0
4848
/svg/actors/vehicle.svg - "Ship's wheel" by Delapouite under CC BY 3.0
49+
/svg/behaviors/difficult-terrain.svg - "Falling rocks" by Delapouite under CC BY 3.0
4950
/svg/damage/acid.svg - "Fizzling flask" by Lorc under CC BY 3.0
5051
/svg/damage/all.svg - "Triple plier" by Lorc under CC BY 3.0
5152
/svg/damage/bludgeoning.svg - "Claw hammer" by Lorc under CC BY 3.0
Lines changed: 6 additions & 0 deletions
Loading

lang/en.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,24 @@
276276
"other": "Activities"
277277
},
278278
"FIELDS": {
279+
"behaviors": {
280+
"element": {
281+
"level": {
282+
"label": "Level Limit",
283+
"hint": "Range of levels required to apply this behavior.",
284+
"max": {
285+
"label": "Maximum Level"
286+
},
287+
"min": {
288+
"label": "Minimum Level"
289+
}
290+
},
291+
"name": {
292+
"label": "Name"
293+
}
294+
},
295+
"label": "Applied Behaviors"
296+
},
279297
"description": {
280298
"label": "Description",
281299
"chatFlavor": {
@@ -1056,6 +1074,15 @@
10561074
}
10571075
},
10581076

1077+
"DND5E.BEHAVIOR": {
1078+
"Action": {
1079+
"Create": "Create Behavior",
1080+
"Delete": "Delete Behavior"
1081+
},
1082+
"Hint": "These behaviors will apply to any area of effect placed by this activity.",
1083+
"Label": "Behavior"
1084+
},
1085+
10591086
"DND5E.Biography": "Biography",
10601087
"DND5E.BiographyPublic": "Public Biography",
10611088
"DND5E.BiopgrahyPublicEdit": "Edit Public Biography",

module/_types.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@
4747

4848
/* -------------------------------------------- */
4949

50+
/**
51+
* @typedef ActivityBehaviorConfiguration
52+
* @param {string} label Localized label for the behavior.
53+
* @param {string} icon Icon representing the behavior.
54+
*/
55+
56+
/* -------------------------------------------- */
57+
5058
/**
5159
* @typedef ActivityConsumptionTargetConfiguration
5260
* @property {string} label Localized label for the target type.

module/applications/activity/activity-sheet.mjs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import CreateDocumentDialog from "../create-document-dialog.mjs";
12
import { ConsumptionTargetData } from "../../data/activity/fields/consumption-targets-field.mjs";
3+
import BaseActivityBehavior from "../../data/region-behavior/base-activity-behavior.mjs";
24
import UsesField from "../../data/shared/uses-field.mjs";
35
import PseudoDocumentSheet from "../api/pseudo-document-sheet.mjs";
46

@@ -13,10 +15,12 @@ export default class ActivitySheet extends PseudoDocumentSheet {
1315
icon: "fa-solid fa-gauge"
1416
},
1517
actions: {
18+
addBehavior: ActivitySheet.#addBehavior,
1619
addConsumption: ActivitySheet.#addConsumption,
1720
addDamagePart: ActivitySheet.#addDamagePart,
1821
addEffect: ActivitySheet.#addEffect,
1922
addRecovery: ActivitySheet.#addRecovery,
23+
deleteBehavior: ActivitySheet.#deleteBehavior,
2024
deleteConsumption: ActivitySheet.#deleteConsumption,
2125
deleteDamagePart: ActivitySheet.#deleteDamagePart,
2226
deleteEffect: ActivitySheet.#deleteEffect,
@@ -54,6 +58,9 @@ export default class ActivitySheet extends PseudoDocumentSheet {
5458
effect: {
5559
template: "systems/dnd5e/templates/activity/effect.hbs",
5660
templates: [
61+
"systems/dnd5e/templates/activity/parts/activity-behaviors.hbs",
62+
"systems/dnd5e/templates/activity/parts/activity-behavior-level-limit.hbs",
63+
"systems/dnd5e/templates/activity/parts/activity-behavior-settings.hbs",
5764
"systems/dnd5e/templates/activity/parts/activity-effects.hbs",
5865
"systems/dnd5e/templates/activity/parts/activity-effect-level-limit.hbs",
5966
"systems/dnd5e/templates/activity/parts/activity-effect-settings.hbs"
@@ -230,6 +237,19 @@ export default class ActivitySheet extends PseudoDocumentSheet {
230237

231238
/* -------------------------------------------- */
232239

240+
/**
241+
* Prepare a specific applied behavior if present in the activity data.
242+
* @param {ApplicationRenderContext} context Context being prepared.
243+
* @param {object} behavior Applied behavior context being prepared.
244+
* @returns {object}
245+
* @protected
246+
*/
247+
_prepareAppliedBehaviorContext(context, behavior) {
248+
return behavior;
249+
}
250+
251+
/* -------------------------------------------- */
252+
233253
/**
234254
* Prepare a specific applied effect if present in the activity data.
235255
* @param {ApplicationRenderContext} context Context being prepared.
@@ -266,6 +286,26 @@ export default class ActivitySheet extends PseudoDocumentSheet {
266286
async _prepareEffectContext(context, options) {
267287
context.tab = context.tabs.effect;
268288

289+
if ( context.activity.behaviors && (context.activity.target?.template?.type || this.activity.isRider) ) {
290+
context.appliedBehaviors = context.activity.behaviors.values()
291+
.filter(d => d.type in CONFIG.DND5E.activityBehaviorTypes)
292+
.map(data => {
293+
const source = context.source.behaviors[data._index];
294+
if ( !source ) return null;
295+
const instance = new CONFIG.DND5E.activityBehaviorTypes[source.type].model(source.config);
296+
const ctx = {
297+
data, source,
298+
additionalFields: instance.generateFields(source.config, { prefix: `behaviors.${data._index}.config.` }),
299+
additionalSettings: "systems/dnd5e/templates/activity/parts/activity-behavior-settings.hbs",
300+
collapsed: this.expandedSections.get(`behavior.${data._id}`) ? "" : "collapsed",
301+
config: CONFIG.DND5E.activityBehaviorTypes[data.type],
302+
fields: this.activity.schema.fields.behaviors.element.fields,
303+
prefix: `behaviors.${data._index}.`
304+
};
305+
return this._prepareAppliedBehaviorContext(context, ctx);
306+
});
307+
}
308+
269309
if ( context.activity.effects ) {
270310
const appliedEffects = new Set(context.activity.effects?.map(e => e._id) ?? []);
271311
context.allEffects = this.item.effects
@@ -462,6 +502,21 @@ export default class ActivitySheet extends PseudoDocumentSheet {
462502

463503
/* -------------------------------------------- */
464504

505+
/**
506+
* Handle adding a new entry to the behaviors list.
507+
* @this {ActivitySheet}
508+
* @param {Event} event Triggering click event.
509+
* @param {HTMLElement} target Button that was clicked.
510+
*/
511+
static async #addBehavior(event, target) {
512+
if ( !this.activity.behaviors ) return;
513+
const createData = await CreateDocumentDialog.prompt(BaseActivityBehavior, {}, { parent: this.activity });
514+
if ( !createData?.type ) return;
515+
this.activity.update({ behaviors: [...this.activity.toObject().behaviors, createData] });
516+
}
517+
518+
/* -------------------------------------------- */
519+
465520
/**
466521
* Handle adding a new entry to the consumption list.
467522
* @this {ActivitySheet}
@@ -551,6 +606,20 @@ export default class ActivitySheet extends PseudoDocumentSheet {
551606

552607
/* -------------------------------------------- */
553608

609+
/**
610+
* Handle removing an entry from the behaviors list.
611+
* @this {ActivitySheet}
612+
* @param {Event} event Triggering click event.
613+
* @param {HTMLElement} target Button that was clicked.
614+
*/
615+
static #deleteBehavior(event, target) {
616+
if ( !this.activity.behaviors ) return;
617+
const behaviors = this.activity.toObject().behaviors;
618+
this.activity.update({ behaviors: behaviors.toSpliced(target.closest("[data-index]").dataset.index, 1) });
619+
}
620+
621+
/* -------------------------------------------- */
622+
554623
/**
555624
* Handle removing an entry from the consumption targets list.
556625
* @this {ActivitySheet}

module/applications/create-document-dialog.mjs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,6 @@ export default class CreateDocumentDialog extends Dialog5e {
9292
if ( this.options.types?.length === 0 ) throw new Error("The array of sub-types to restrict to must not be empty");
9393

9494
for ( const type of TYPES ) {
95-
// TODO: When `standard` AE type replaces `base`, remove this check for active effect
96-
// See https://github.com/foundryvtt/dnd5e/pull/5941
9795
if ( (this.documentName !== "ActiveEffect") && (type === CONST.BASE_DOCUMENT_TYPE) ) continue;
9896
if ( this.options.types && !this.options.types.includes(type) ) continue;
9997
const typeData = { selected: type === defaultType, type };
@@ -112,6 +110,8 @@ export default class CreateDocumentDialog extends Dialog5e {
112110

113111
context.types.sort((a, b) => a.label.localeCompare(b.label, game.i18n.lang));
114112
context.hasTypes = true;
113+
} else if ( (TYPES?.length === 1) && !this.options.createData.type ) {
114+
this.options.createData.type = defaultType ?? TYPES[0];
115115
}
116116

117117
return context;
@@ -159,7 +159,7 @@ export default class CreateDocumentDialog extends Dialog5e {
159159
* @param {DatabaseCreateOperation} [createOptions={}] Document creation options.
160160
* @param {object} [dialogOptions={}] Options forwarded to dialog.
161161
* @param {object} [dialogOptions.ok={}] Options for the OK button.
162-
* @returns {Promise<Document>}
162+
* @returns {Promise<Document|PseudoDocument|object|void>}
163163
*/
164164
static async prompt(documentType, data={}, { folders, types, ...createOptions }={}, { ok={}, ...config }={}) {
165165
const label = game.i18n.localize(documentType.metadata.label ?? `DOCUMENT.DND5E.${documentType.documentName}`);
@@ -190,8 +190,10 @@ export default class CreateDocumentDialog extends Dialog5e {
190190
createOptions.renderSheet ??= true;
191191
if ( foundry.utils.isSubclass(documentType, foundry.abstract.Document) ) {
192192
resolve(documentType.create(createData, createOptions));
193-
} else {
193+
} else if ( createOptions.parent?.[`create${documentType.documentName}`] ) {
194194
resolve(createOptions.parent[`create${documentType.documentName}`](createData.type, createData, createOptions));
195+
} else {
196+
resolve(createData);
195197
}
196198
});
197199
dialog.render({ force: true });

module/config.mjs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ConsumptionTargetData } from "./data/activity/fields/consumption-target
44
import { CalendarGreyhawk, CALENDAR_OF_GREYHAWK } from "./data/calendar/calendar-of-greyhawk.mjs";
55
import { CalendarHarptos, CALENDAR_OF_HARPTOS } from "./data/calendar/calendar-of-harptos.mjs";
66
import { CalendarKhorvaire, CALENDAR_OF_KHORVAIRE } from "./data/calendar/calendar-of-khorvaire.mjs";
7+
import * as regionBehaviors from "./data/region-behavior/_module.mjs";
78
import * as activities from "./documents/activity/_module.mjs";
89
import Actor5e from "./documents/actor/actor.mjs";
910
import * as advancement from "./documents/advancement/_module.mjs";
@@ -1093,6 +1094,21 @@ preLocalize("activityActivationTypes", { key: "label" });
10931094

10941095
/* -------------------------------------------- */
10951096

1097+
/**
1098+
* Types of behaviors that can be attached to regions through activities.
1099+
* @enum {ActivityBehaviorConfiguration}
1100+
*/
1101+
DND5E.activityBehaviorTypes = {
1102+
difficultTerrain: {
1103+
label: "TYPES.RegionBehavior.dnd5e.difficultTerrain",
1104+
icon: "systems/dnd5e/icons/svg/behaviors/difficult-terrain.svg",
1105+
model: regionBehaviors.DifficultTerrainActivityBehavior
1106+
}
1107+
};
1108+
preLocalize("activityBehaviorTypes", { key: "label" });
1109+
1110+
/* -------------------------------------------- */
1111+
10961112
/**
10971113
* Different things that an ability can consume upon use.
10981114
* @enum {string}

module/data/activity/_module.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export {default as SummonActivityData} from "./summon-data.mjs";
1212
export {default as TransformActivityData} from "./transform-data.mjs";
1313
export {default as UtilityActivityData} from "./utility-data.mjs";
1414

15+
export {default as AppliedBehaviorField} from "./fields/applied-behavior-field.mjs";
1516
export {default as AppliedEffectField} from "./fields/applied-effect-field.mjs";
1617
export {
1718
default as ConsumptionTargetsField, ConsumptionTargetData, ConsumptionError

0 commit comments

Comments
 (0)