197 lines
7.2 KiB
JavaScript
197 lines
7.2 KiB
JavaScript
// SPDX-FileCopyrightText: 2021 Johannes Loher
|
|
//
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
import { mathEvaluator } from "../expression-evaluation/evaluator";
|
|
import { getGame } from "../utils/utils";
|
|
|
|
/**
|
|
* @typedef {object} ItemEffectConfig
|
|
* @property {boolean} [applyToItems] Whether or not to apply this effect to owned items instead of the actor
|
|
* @property {string} [itemName] Only apply this effect to items with this name
|
|
* @property {string} [condition] Only apply this effect to items where this condition is fullfilled
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} DS4ActiveEffectFlags
|
|
* @property {ItemEffectConfig} [itemEffectConfig] Configuration for applying this effect to owned items
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Record<string, unknown>} ActiveEffectFlags
|
|
* @property {DS4ActiveEffectFlags} [ds4] Flags for DS4
|
|
*/
|
|
|
|
export class DS4ActiveEffect extends ActiveEffect {
|
|
/**
|
|
* A fallback icon that can be used if no icon is defined for the effect.
|
|
*/
|
|
static FALLBACK_ICON = "icons/svg/aura.svg";
|
|
|
|
/**
|
|
* A cached reference to the source document to avoid recurring database lookups
|
|
* @type {foundry.abstract.Document | undefined | null}
|
|
* @protected
|
|
*/
|
|
source = undefined;
|
|
|
|
/**
|
|
* Whether or not this effect is currently surpressed.
|
|
* @type {boolean}
|
|
*/
|
|
get isSurpressed() {
|
|
const originatingItem = this.originatingItem;
|
|
if (!originatingItem) {
|
|
return false;
|
|
}
|
|
return originatingItem.isNonEquippedEuipable();
|
|
}
|
|
|
|
/**
|
|
* The item which this effect originates from if it has been transferred from an item to an actor.
|
|
* @return {import("./item/item").DS4Item | undefined}
|
|
*/
|
|
get originatingItem() {
|
|
if (!(this.parent instanceof Actor)) {
|
|
return;
|
|
}
|
|
const itemIdRegex = /Item\.([a-zA-Z0-9]+)/;
|
|
const itemId = this.origin?.match(itemIdRegex)?.[1];
|
|
if (!itemId) {
|
|
return;
|
|
}
|
|
return this.parent.items.get(itemId);
|
|
}
|
|
|
|
/**
|
|
* The number of times this effect should be applied.
|
|
* @type {number}
|
|
*/
|
|
get factor() {
|
|
return this.originatingItem?.activeEffectFactor ?? 1;
|
|
}
|
|
|
|
/** @override */
|
|
apply(document, change) {
|
|
change.value = Roll.replaceFormulaData(change.value, document);
|
|
try {
|
|
change.value = DS4ActiveEffect.safeEval(change.value).toString();
|
|
} catch (e) {
|
|
// this is a valid case, e.g., if the effect change simply is a string
|
|
}
|
|
return super.apply(document, change);
|
|
}
|
|
|
|
/**
|
|
* Gets the current source name based on the cached source object.
|
|
* @returns {Promise<string>} The current source name
|
|
*/
|
|
async getCurrentSourceName() {
|
|
const game = getGame();
|
|
const origin = await this.getSource();
|
|
if (origin === null) return game.i18n.localize("None");
|
|
return origin.name ?? game.i18n.localize("Unknown");
|
|
}
|
|
|
|
/**
|
|
* Gets the source document for this effect. Uses the cached {@link DS4ActiveEffect#source} if it has already been
|
|
* set.
|
|
* @protected
|
|
* @returns {Promise<foundry.abstract.Document | null>}
|
|
*/
|
|
async getSource() {
|
|
if (this.source === undefined) {
|
|
this.source = this.origin != null ? await fromUuid(this.origin) : null;
|
|
}
|
|
return this.source;
|
|
}
|
|
|
|
/**
|
|
* Create a new {@link DS4ActiveEffect} using default values.
|
|
*
|
|
* @param {import("./item/item").DS4Item | import("./actor/actor").DS4Actor} parent The parent of the effect.
|
|
* @returns {Promise<DS4ActiveEffect | undefined>} A promise that resolved to the created effect or udifined of the
|
|
* creation was prevented.
|
|
*/
|
|
static async createDefault(parent) {
|
|
const createData = {
|
|
label: getGame().i18n.localize(`DS4.NewEffectLabel`),
|
|
icon: this.FALLBACK_ICON,
|
|
};
|
|
|
|
return this.create(createData, { parent, pack: parent.pack ?? undefined });
|
|
}
|
|
|
|
/**
|
|
* Safely evaluate a mathematical expression.
|
|
* @param {string} expression The expression to evaluate
|
|
* @returns {number | `${number | boolean}`} The numeric result of the expression
|
|
* @throws If the expression could not be evaluated or did not produce a numeric resilt
|
|
*/
|
|
static safeEval(expression) {
|
|
const result = mathEvaluator.evaluate(expression);
|
|
if (!Number.isNumeric(result)) {
|
|
throw new Error(`mathEvaluator.evaluate produced a non-numeric result from expression "${expression}"`);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Apply the given effects to the gicen Actor or item.
|
|
* @param {import("./item/item").DS4Item | import("./actor/actor").DS4Actor} document The Actor or Item to which to apply the effects
|
|
* @param {DS4ActiveEffect[]} effetcs The effects to apply
|
|
* @param {(change: EffectChangeData) => boolean} [predicate=() => true] Apply only changes that fullfill this predicate
|
|
*/
|
|
static applyEffetcs(document, effetcs, predicate = () => true) {
|
|
/** @type {Record<string, unknown>} */
|
|
const overrides = {};
|
|
|
|
// Organize non-disabled and -surpressed effects by their application priority
|
|
const changesWithEffect = effetcs.flatMap((e) => e.getFactoredChangesWithEffect(predicate));
|
|
changesWithEffect.sort((a, b) => (a.change.priority ?? 0) - (b.change.priority ?? 0));
|
|
|
|
// Apply all changes
|
|
for (const changeWithEffect of changesWithEffect) {
|
|
if (!changeWithEffect.change.key) continue;
|
|
const changes = changeWithEffect.effect.apply(document, changeWithEffect.change);
|
|
Object.assign(overrides, changes);
|
|
}
|
|
|
|
// Expand the set of final overrides
|
|
document.overrides = foundry.utils.expandObject({
|
|
...foundry.utils.flattenObject(document.overrides),
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the array of changes for this effect, considering the {@link DS4ActiveEffect#factor}.
|
|
* @param {(change: EffectChangeData) => boolean} [predicate=() => true] An optional predicate to filter which changes should be considered
|
|
* @returns {EffectChangeDataWithEffect[]} The array of changes from this effect, considering the factor.
|
|
* @protected
|
|
*/
|
|
getFactoredChangesWithEffect(predicate = () => true) {
|
|
if (this.disabled || this.isSurpressed) {
|
|
return [];
|
|
}
|
|
|
|
return this.changes.filter(predicate).flatMap((change) => {
|
|
change.priority = change.priority ?? change.mode * 10;
|
|
return Array(this.factor).fill({ effect: this, change });
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @typedef {object} EffectChangeDataWithEffect
|
|
* @property {DS4ActiveEffect} effect
|
|
* @property {EffectChangeData} change
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} EffectChangeData
|
|
* @property {string} key The attribute path in the Actor or Item data which the change modifies
|
|
* @property {string} value The value of the change effect
|
|
* @property {number} mode The modification mode with which the change is applied
|
|
* @property {number} priority The priority level with which this change is applied
|
|
*/
|