// SPDX-FileCopyrightText: 2021 Johannes Loher // // SPDX-License-Identifier: MIT import { mathEvaluator } from "../expression-evaluation/evaluator"; import { getGame } from "../helpers"; import type { DS4Actor } from "../actor/actor"; import type { DS4Item } from "../item/item"; declare global { interface DocumentClassConfig { ActiveEffect: typeof DS4ActiveEffect; } interface FlagConfig { ActiveEffect: { ds4?: { itemEffectConfig?: { applyToItems?: boolean; itemName?: string; condition?: string; }; }; }; } } type PromisedType = T extends Promise ? U : T; 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 */ protected source: PromisedType> | undefined = undefined; /** * Whether or not this effect is currently surpressed. */ get isSurpressed(): boolean { 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. */ get originatingItem(): DS4Item | undefined { if (!(this.parent instanceof Actor)) { return; } const itemIdRegex = /Item\.([a-zA-Z0-9]+)/; const itemId = this.data.origin?.match(itemIdRegex)?.[1]; if (!itemId) { return; } return this.parent.items.get(itemId); } /** * The number of times this effect should be applied. */ get factor(): number { return this.originatingItem?.activeEffectFactor ?? 1; } override apply(document: DS4Actor | DS4Item, change: EffectChangeData): unknown { change.value = Roll.replaceFormulaData(change.value, document.data); 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 } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error In the types and foundry's documentation, only actors are allowed, but the implementation actually works for all kinds of documents return super.apply(document, change); } /** * Gets the current source name based on the cached source object. */ async getCurrentSourceName(): Promise { 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 async getSource(): ReturnType { if (this.source === undefined) { this.source = this.data.origin !== undefined ? await fromUuid(this.data.origin) : null; } return this.source; } /** * Create a new {@link DS4ActiveEffect} using default data. * * @param parent The parent {@link DS4Actor} or {@link DS4Item} of the effect. * @returns A promise that resolved to the created effect or udifined of the creation was prevented. */ static async createDefault(parent: DS4Actor | DS4Item): Promise { const createData = { label: getGame().i18n.localize(`DS4.NewEffectLabel`), icon: this.FALLBACK_ICON, }; return this.create(createData, { parent, pack: parent.pack ?? undefined }); } static safeEval(expression: string): number | `${number | boolean}` { const result = mathEvaluator.evaluate(expression); if (!Number.isNumeric(result)) { throw new Error(`mathEvaluator.evaluate produced a non-numeric result from expression "${expression}"`); } return result as number | `${number | boolean}`; } /** * Apply the given effects to the gicen Actor or item. * @param document The Actor or Item to which to apply the effects * @param effetcs The effects to apply * @param predicate Apply only changes that fullfill this predicate */ static applyEffetcs( document: DS4Actor | DS4Item, effetcs: DS4ActiveEffect[], predicate: (change: EffectChangeData) => boolean = () => true, ): void { const overrides: Record = {}; // 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) { const result = changeWithEffect.effect.apply(document, changeWithEffect.change); if (result !== null) overrides[changeWithEffect.change.key] = result; } // 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 predicate An optional predicate to filter which changes should be considered * @returns The array of changes from this effect, considering the factor. */ protected getFactoredChangesWithEffect( predicate: (change: EffectChangeData) => boolean = () => true, ): EffectChangeDataWithEffect[] { if (this.data.disabled || this.isSurpressed) { return []; } return this.data.changes.filter(predicate).flatMap((change) => { change.priority = change.priority ?? change.mode * 10; return Array(this.factor).fill({ effect: this, change }); }); } } type EffectChangeData = foundry.data.ActiveEffectData["changes"][number]; type EffectChangeDataWithEffect = { effect: DS4ActiveEffect; change: EffectChangeData };