// SPDX-FileCopyrightText: 2021 Johannes Loher // // SPDX-License-Identifier: MIT import { DS4Actor } from "./actor/actor"; import { mathEvaluator } from "./expression-evaluation/evaluator"; import { getGame } from "./helpers"; import type { DS4Item } from "./item/item"; declare global { interface DocumentClassConfig { ActiveEffect: typeof DS4ActiveEffect; } } 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 DS4Actor)) { 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(actor: DS4Actor, change: foundry.data.ActiveEffectData["changes"][number]): unknown { change.value = Roll.replaceFormulaData(change.value, actor.data); try { change.value = DS4ActiveEffect.safeEval(change.value).toString(); } catch (e) { logger.warn(e); // this is a valid case, e.g., if the effect change simply is a string } return super.apply(actor, 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}`; } }