import { itemAbleRollParse } from '../helpers/utils.mjs'; import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs'; export default class DhActiveEffect extends foundry.documents.ActiveEffect { /* -------------------------------------------- */ /* Properties */ /* -------------------------------------------- */ /**@override */ get isSuppressed() { // If this is a copied effect from an attachment, never suppress it // (These effects have attachmentSource metadata) if (this.flags?.daggerheart?.attachmentSource) { return false; } // Then apply the standard suppression rules if (['weapon', 'armor'].includes(this.parent?.type)) { return !this.parent.system.equipped; } if (this.parent?.type === 'domainCard') { return this.parent.system.inVault; } return super.isSuppressed; } /** * Check if the parent item is currently attached to another item * @returns {boolean} */ get isAttached() { if (!this.parent || !this.parent.parent) return false; // Check if this item's UUID is in any actor's armor or weapon attachment lists const actor = this.parent.parent; if (!actor || !actor.items) return false; return actor.items.some(item => { return ( (item.type === 'armor' || item.type === 'weapon') && item.system?.attached && Array.isArray(item.system.attached) && item.system.attached.includes(this.parent.uuid) ); }); } /* -------------------------------------------- */ /* Event Handlers */ /* -------------------------------------------- */ /**@inheritdoc*/ async _preCreate(data, options, user) { const update = {}; if (!data.img) { update.img = 'icons/magic/life/heart-cross-blue.webp'; } const immuneStatuses = data.statuses?.filter( status => this.parent.system.rules?.conditionImmunities && this.parent.system.rules.conditionImmunities[status] ) ?? []; if (immuneStatuses.length > 0) { update.statuses = data.statuses.filter(x => !immuneStatuses.includes(x)); const conditions = CONFIG.DH.GENERAL.conditions(); const scrollingTexts = immuneStatuses.map(status => ({ text: game.i18n.format('DAGGERHEART.ACTIVEEFFECT.immuneStatusText', { status: game.i18n.localize(conditions[status].name) }) })); if (update.statuses.length > 0) { setTimeout(() => scrollingTexts, 500); } else { this.parent.queueScrollText(scrollingTexts); } } if (Object.keys(update).length > 0) { await this.updateSource(update); } await super._preCreate(data, options, user); } /** @inheritdoc */ _onCreate(data, options, userId) { super._onCreate(data, options, userId); Hooks.callAll(RefreshType.EffectsDisplay); } /** @inheritdoc */ _onDelete(data, options, userId) { super._onDelete(data, options, userId); Hooks.callAll(RefreshType.EffectsDisplay); } /* -------------------------------------------- */ /* Methods */ /* -------------------------------------------- */ /**@inheritdoc*/ static applyField(model, change, field) { const isOriginTarget = change.value.toLowerCase().includes('origin.@'); let parseModel = model; if (isOriginTarget && change.effect.origin) { change.value = change.value.replaceAll(/origin\.@/gi, '@'); try { const effect = foundry.utils.fromUuidSync(change.effect.origin); const doc = effect.parent?.parent instanceof game.system.api.documents.DhpActor ? effect.parent : effect.parent.parent; if (doc) parseModel = doc; } catch (_) {} } const evalValue = this.effectSafeEval(itemAbleRollParse(change.value, parseModel, change.effect.parent)); change.value = evalValue ?? change.value; super.applyField(model, change, field); } /** * Altered Foundry safeEval to allow non-numeric return * @param {string} expression * @returns */ static effectSafeEval(expression) { let result; try { // eslint-disable-next-line no-new-func const evl = new Function('sandbox', `with (sandbox) { return ${expression}}`); result = evl(Roll.MATH_PROXY); } catch (err) { return expression; } return result; } /** * Generates a list of localized tags based on this item's type-specific properties. * @returns {string[]} An array of localized tag strings. */ _getTags() { const tags = [ `${game.i18n.localize(this.parent.system.metadata.label)}: ${this.parent.name}`, game.i18n.localize( this.isTemporary ? 'DAGGERHEART.EFFECTS.Duration.temporary' : 'DAGGERHEART.EFFECTS.Duration.passive' ) ]; for (const statusId of this.statuses) { const status = CONFIG.statusEffects.find(s => s.id === statusId); if (status) tags.push(game.i18n.localize(status.name)); } return tags; } /** * Create a new ChatMessage to display this document’s data. * @param {String} origin - uuid of a document. TODO: This needs to be reviewed. */ async toChat(origin) { /**@type {foundry.documents.ChatMessage} */ const cls = getDocumentClass('ChatMessage'); const speaker = cls.getSpeaker(); const actor = cls.getSpeakerActor(speaker); const systemData = { action: { img: this.img, name: this.name }, actor: { name: actor?.name, img: actor?.img }, speaker, origin, description: this.description, actions: [] }; const msg = { title: game.i18n.localize('DAGGERHEART.GENERAL.Effect.single'), user: game.user.id, system: systemData, content: await foundry.applications.handlebars.renderTemplate( 'systems/daggerheart/templates/ui/chat/action.hbs', systemData ) }; cls.create(msg); } prepareDerivedData() { /* Preventing subclass features from transferring to actor if they do not have the right subclass advancement */ if (this.parent?.type === 'feature') { const origSubclassParent = this.parent.system.originItemType === 'subclass'; if (origSubclassParent) { const subclass = this.parent.parent.items.find( x => x.type === 'subclass' && x.system.isMulticlass === (this.parent.system.identifier === 'multiclass') ); if (subclass) { const featureState = subclass.system.featureState; if ( (this.parent.system.identifier === CONFIG.DH.ITEM.featureSubTypes.specialization && featureState < 2) || (this.parent.system.identifier === CONFIG.DH.ITEM.featureSubTypes.mastery && featureState < 3) ) { this.transfer = false; } } } } } }