Skip to content

Commit ade1639

Browse files
committed
[#6154] Add new Region Behavior type for condition AE application
Adds a new Region Behavior that behaves the same as core's Apply Active Effect behavior but with some extra conditions that are evaluated before applying the effect. Tokens can be filtered by disposition, size, and creature type. Closes #6154
1 parent d7aa5ce commit ade1639

5 files changed

Lines changed: 155 additions & 4 deletions

File tree

lang/en.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,14 @@
5858
"TYPES.Item.weaponPl": "Weapons",
5959

6060
"TYPES.RegionBehavior": {
61+
"dnd5e.applyActiveEffect": "Apply Active Effect (5e)",
6162
"dnd5e.difficultTerrain": "Difficult Terrain",
6263
"dnd5e.rotateArea": "Rotate Area"
6364
},
6465

6566
"TYPES.HINTS": {
6667
"RegionBehavior": {
68+
"dnd5e.applyActiveEffect": "Apply Active Effects to Tokens while they are within the Region and meet certain criteria.",
6769
"dnd5e.difficultTerrain": "Mark a region as being difficult terrain and set its type and who it affects.",
6870
"dnd5e.rotateArea": "Rotate tokens, tiles, walls, and other objects around a central pivot point."
6971
}
@@ -3592,6 +3594,26 @@
35923594
"DND5E.RecoveryFormula": "Recovery Formula",
35933595

35943596
"DND5E.REGIONBEHAVIORS": {
3597+
"APPLYACTIVEEFFECT": {
3598+
"FIELDS": {
3599+
"dispositions": {
3600+
"hint": "If set, only apply effects to Tokens with these dispositions.",
3601+
"label": "Dispositions"
3602+
},
3603+
"effects": {
3604+
"hint": "The effects that are applied to Tokens within the Region.",
3605+
"label": "Effects"
3606+
},
3607+
"sizes": {
3608+
"hint": "If set, only apply effects to Tokens that are these sizes.",
3609+
"label": "Sizes"
3610+
},
3611+
"types": {
3612+
"hint": "If set, only apply effects to Tokens that are these creature types.",
3613+
"label": "Types"
3614+
}
3615+
}
3616+
},
35953617
"DIFFICULTTERRAIN": {
35963618
"FIELDS": {
35973619
"ignoredDispositions": {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
1+
import {default as ApplyActiveEffect5eRegionBehaviorType} from "./apply-active-effect.mjs";
12
import {default as DifficultTerrainRegionBehaviorType} from "./difficult-terrain.mjs";
23
import {default as RotateAreaRegionBehaviorType} from "./rotate-area.mjs";
34

45
export {
6+
ApplyActiveEffect5eRegionBehaviorType,
57
DifficultTerrainRegionBehaviorType,
68
RotateAreaRegionBehaviorType
79
};
810

911
export const config = {
12+
"dnd5e.applyActiveEffect": ApplyActiveEffect5eRegionBehaviorType,
1013
"dnd5e.difficultTerrain": DifficultTerrainRegionBehaviorType,
1114
"dnd5e.rotateArea": RotateAreaRegionBehaviorType
1215
};
1316

1417
export const icons = {
18+
"dnd5e.applyActiveEffect": "fa-solid fa-person-rays",
1519
"dnd5e.difficultTerrain": "fa-solid fa-hill-rockslide",
1620
"dnd5e.rotateArea": "fa-solid fa-arrows-spin"
1721
};

module/data/region-behavior/_types.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
1+
/**
2+
* @typedef ApplyActiveEffectRegionBehaviorSystemData
3+
* @property {Set<string>} effects UUIDs of effects to apply.
4+
* @property {Set<number>} dispositions If not empty, only apply effects to tokens with these dispositions.
5+
* @property {Set<string>} sizes If not empty, only apply effects to tokens with these sizes.
6+
* @property {Set<string>} types If not empty, only apply effects to tokens with these creature types.
7+
*/
8+
9+
/* ---------------------------------------- */
10+
111
/**
212
* @typedef DifficultTerrainRegionBehaviorSystemData
313
* @property {boolean} magical This difficult terrain is caused by magic.
414
* @property {Set<string>} types Types of difficult terrain represented.
515
* @property {Set<number>} ignoredDispositions Token dispositions that won't be affected by this difficult terrain.
616
*/
717

18+
/* ---------------------------------------- */
19+
820
/**
921
* @typedef RotateAreaRegionBehaviorSystemData
1022
* @property {object} time
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
const { DocumentUUIDField, NumberField, SetField, StringField } = foundry.data.fields;
2+
3+
/**
4+
* @import { ApplyActiveEffectRegionBehaviorSystemData } from "./_types.mjs";
5+
*/
6+
7+
/**
8+
* The data model for a region behavior that applies active effects to certain tokens.
9+
* @extends {foundry.data.regionBehaviors.RegionBehaviorType<ApplyActiveEffectRegionBehaviorSystemData>}
10+
* @mixes ApplyActiveEffectRegionBehaviorSystemData
11+
*/
12+
export default class ApplyActiveEffect5eRegionBehaviorType extends foundry.data.regionBehaviors.RegionBehaviorType {
13+
14+
/** @override */
15+
static LOCALIZATION_PREFIXES = ["DND5E.REGIONBEHAVIORS.APPLYACTIVEEFFECT"];
16+
17+
/* ---------------------------------------- */
18+
19+
/** @override */
20+
static defineSchema() {
21+
const dispositions = { ...foundry.applications.sheets.TokenConfig.TOKEN_DISPOSITIONS };
22+
delete dispositions[CONST.TOKEN_DISPOSITIONS.SECRET];
23+
return {
24+
effects: new SetField(new DocumentUUIDField({ type: "ActiveEffect", nullable: false })),
25+
dispositions: new SetField(new NumberField({ choices: dispositions })),
26+
sizes: new SetField(new StringField({ choices: () => CONFIG.DND5E.actorSizes })),
27+
types: new SetField(new StringField({ choices: () => CONFIG.DND5E.creatureTypes }))
28+
// TODO: Add FiltersField for arbitrary conditions once https://github.com/foundryvtt/dnd5e/issues/6672 is done
29+
};
30+
}
31+
32+
/* ---------------------------------------- */
33+
/* Methods */
34+
/* ---------------------------------------- */
35+
36+
/**
37+
* Check the conditions to decide if effects should be added to this token.
38+
* @param {TokenDocument5e} token The token to which to add the effects.
39+
* @returns {boolean}
40+
*/
41+
#evaluateConditions(token) {
42+
if ( this.dispositions.size && !this.dispositions.has(token.disposition) ) return false;
43+
if ( this.sizes.size && !this.sizes.has(token.actor.system.traits?.size) ) return false;
44+
if ( this.types.size && !this.types.has(token.actor.system.details?.type?.value) ) return false;
45+
return true;
46+
}
47+
48+
/* ---------------------------------------- */
49+
50+
/**
51+
* Apply the Active Effects when the Token enters the Region.
52+
* @param {RegionTokenEnterEvent} event
53+
* @this {ApplyActiveEffect5eRegionBehaviorType}
54+
*/
55+
static async #onTokenEnter(event) {
56+
if ( !event.user.isSelf ) return;
57+
const { token, movement } = event.data;
58+
const actor = token.actor;
59+
if ( !actor || !this.#evaluateConditions(token) ) return;
60+
const resumeMovement = movement ? token.pauseMovement() : undefined;
61+
const effects = await Promise.all(this.effects.map(fromUuid));
62+
const toCreate = [];
63+
for ( const effect of effects ) {
64+
const data = effect.toObject();
65+
delete data._id;
66+
if ( effect.compendium ) {
67+
data._stats.duplicateSource = null;
68+
data._stats.compendiumSource = effect.uuid;
69+
} else {
70+
data._stats.duplicateSource = effect.uuid;
71+
data._stats.compendiumSource = null;
72+
}
73+
data._stats.exportSource = null;
74+
data.origin = this.parent.uuid;
75+
toCreate.push(data);
76+
}
77+
if ( toCreate.length ) await actor.createEmbeddedDocuments("ActiveEffect", toCreate);
78+
await resumeMovement?.();
79+
}
80+
81+
/* ---------------------------------------- */
82+
83+
/**
84+
* Un-apply the Active Effects when the Token exists the Region.
85+
* @param {RegionTokenExitEvent} event
86+
* @this {ApplyActiveEffect5eRegionBehaviorType}
87+
*/
88+
static async #onTokenExit(event) {
89+
if ( !event.user.isSelf ) return;
90+
const { token, movement } = event.data;
91+
const actor = token.actor;
92+
if ( !actor ) return;
93+
const toDelete = [];
94+
for ( const effect of actor.effects ) {
95+
if ( effect.origin === this.parent.uuid ) {
96+
toDelete.push(effect.id);
97+
}
98+
}
99+
if ( !toDelete.length ) return;
100+
const resumeMovement = movement ? token.pauseMovement() : undefined;
101+
await actor.deleteEmbeddedDocuments("ActiveEffect", toDelete);
102+
await resumeMovement?.();
103+
}
104+
105+
/* ---------------------------------------- */
106+
107+
/** @override */
108+
static events = {
109+
[CONST.REGION_EVENTS.TOKEN_ENTER]: this.#onTokenEnter,
110+
[CONST.REGION_EVENTS.TOKEN_EXIT]: this.#onTokenExit
111+
};
112+
}

system.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
"id": "dnd5e",
33
"title": "Dungeons & Dragons Fifth Edition",
44
"description": "A system for playing the fifth edition of the world's most popular role-playing game in the Foundry Virtual Tabletop environment.",
5-
"version": "5.3.0",
5+
"version": "6.0.0",
66
"compatibility": {
7-
"minimum": "13.347",
8-
"verified": "13"
7+
"minimum": "14.355",
8+
"verified": "14"
99
},
1010
"url": "https://github.com/foundryvtt/dnd5e/",
1111
"manifest": "https://raw.githubusercontent.com/foundryvtt/dnd5e/master/system.json",
12-
"download": "https://github.com/foundryvtt/dnd5e/releases/download/release-5.3.0/dnd5e-release-5.3.0.zip",
12+
"download": "https://github.com/foundryvtt/dnd5e/releases/download/release-6.0.0/dnd5e-release-6.0.0.zip",
1313
"authors": [
1414
{
1515
"name": "Atropos",
@@ -150,6 +150,7 @@
150150
}
151151
},
152152
"RegionBehavior": {
153+
"dnd5e.applyActiveEffect": {},
153154
"dnd5e.difficultTerrain": {},
154155
"dnd5e.rotateArea": {}
155156
}

0 commit comments

Comments
 (0)