feat: add functionality to apply Active Affects to owned Items

In the Active Effect Config, there are now additional inputs to configure the effect
to be applied to items owned by the actor instead of the actor itself. It is possible
to select the items to which to apply the effect via matching by name, or via a condition
expression, that provides similar capabilities as the evaluation of mathematical
expressions in rolls. Data from the Actor, Item, and Active Effect can be accessed
similar to how properties are accessed in roll formulas (using the prefixes `@actor`,
`@item`, and `@effect`). For example, in order to apply an effect to all ranged
weapons, the conditions would be
```js
'@item.type' === 'weapon' && '@item.data.attackType' === 'ranged'
```
This commit is contained in:
Johannes Loher 2022-08-25 03:31:30 +02:00
parent 27b6506847
commit b1ed05a796
14 changed files with 414 additions and 42 deletions

View file

@ -4,7 +4,9 @@
// SPDX-License-Identifier: MIT
import { DS4 } from "../config";
import { mathEvaluator } from "../expression-evaluation/evaluator";
import { getGame } from "../helpers";
import logger from "../logger";
import { createCheckRoll } from "../rolls/check-factory";
import { isAttribute, isTrait } from "./actor-data-source-base";
@ -14,7 +16,7 @@ import type { DS4Item } from "../item/item";
import type { ItemType } from "../item/item-data-source";
import type { DS4ShieldDataProperties } from "../item/shield/shield-data-properties";
import type { Check } from "./actor-data-properties-base";
import type { EffectChangeData } from "../active-effect/active-effect";
declare global {
interface DocumentClassConfig {
Actor: typeof DS4Actor;
@ -25,7 +27,10 @@ declare global {
* The Actor class for DS4
*/
export class DS4Actor extends Actor {
initialized: boolean | undefined;
override prepareData(): void {
this.initialized = true;
this.data.reset();
this.prepareBaseData();
this.prepareEmbeddedDocuments();
@ -54,6 +59,41 @@ export class DS4Actor extends Actor {
);
}
private get actorEffects() {
return this.effects.filter((effect) => !effect.data.flags.ds4?.itemEffectConfig?.applyToItems);
}
/**
* Get the effects of this actor that should be applied to the given item.
* @param item The item for which to get effects
* @returns The array of effects that are candidates to be applied to the item
*/
itemEffects(item: DS4Item) {
return this.effects.filter((effect) => {
const { applyToItems, itemName, condition } = effect.data.flags.ds4?.itemEffectConfig ?? {};
if (!applyToItems || (itemName !== undefined && itemName !== "" && itemName !== item.name)) {
return false;
}
if (condition !== undefined && condition !== "") {
try {
const replacedCondition = Roll.replaceFormulaData(condition, {
item: item.data,
actor: this.data,
effect: effect.data,
});
return Boolean(mathEvaluator.evaluate(replacedCondition));
} catch (error) {
logger.warn(error);
return false;
}
}
return true;
});
}
/**
* We override this with an empty implementation because we have our own custom way of applying
* {@link ActiveEffect}s and {@link Actor#prepareEmbeddedDocuments} calls this.
@ -81,31 +121,17 @@ export class DS4Actor extends Actor {
*
* @param predicate - The predicate that ActiveEffectChanges need to satisfy in order to be applied
*/
applyActiveEffectsFiltered(predicate: (change: foundry.data.ActiveEffectData["changes"][number]) => boolean): void {
applyActiveEffectsFiltered(predicate: (change: EffectChangeData) => boolean): void {
const overrides: Record<string, unknown> = {};
// Organize non-disabled and -surpressed effects by their application priority
const changes: (foundry.data.ActiveEffectData["changes"][number] & { effect: ActiveEffect })[] =
this.effects.reduce(
(changes: (foundry.data.ActiveEffectData["changes"][number] & { effect: ActiveEffect })[], e) => {
if (e.data.disabled || e.isSurpressed) return changes;
const newChanges = e.data.changes.filter(predicate).flatMap((c) => {
const changeSource = c.toObject();
changeSource.priority = changeSource.priority ?? changeSource.mode * 10;
return Array(e.factor).fill({ ...changeSource, effect: e });
});
return changes.concat(newChanges);
},
[],
);
changes.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
const changesWithEffect = this.actorEffects.flatMap((e) => e.getFactoredChangesWithEffect(predicate));
changesWithEffect.sort((a, b) => (a.change.priority ?? 0) - (b.change.priority ?? 0));
// Apply all changes
for (const change of changes) {
const result = change.effect.apply(this, change);
if (result !== null) overrides[change.key] = result;
for (const changeWithEffect of changesWithEffect) {
const result = changeWithEffect.effect.apply(this, changeWithEffect.change);
if (result !== null) overrides[changeWithEffect.change.key] = result;
}
// Expand the set of final overrides