// 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} 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} 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} */ 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} 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} */ 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 */