diff --git a/scripts/actions/apply-status-action.js b/scripts/actions/apply-status-action.js index 9129153..f50a8b7 100644 --- a/scripts/actions/apply-status-action.js +++ b/scripts/actions/apply-status-action.js @@ -1,18 +1,18 @@ import { MODULE_ID } from "../constants.js"; export async function executeApplyStatusAction(action, context) { - const actor = context.targetActor ?? context.actor; + const actor = context.targetTokenDocument?.actor ?? context.tokenDocument?.actor ?? context.targetActor ?? context.actor; if (!actor) return { success: false, consumed: false, reason: "NO_ACTOR" }; const statuses = action.statuses ?? []; if (!statuses.length) return { success: false, consumed: false, reason: "NO_STATUSES" }; const effects = statuses.map(statusId => { - const statusConfig = CONFIG.statusEffects.find(status => status.id === statusId); + const statusConfig = getStatusEffectConfig(statusId); return { name: statusConfig?.name ? game.i18n.localize(statusConfig.name) : statusId, - icon: statusConfig?.img ?? "icons/svg/aura.svg", + icon: statusConfig?.img ?? statusConfig?.icon ?? "icons/svg/aura.svg", statuses: [statusId], origin: context.reactionOrigin, duration: buildEffectDuration(action.duration), @@ -31,6 +31,13 @@ export async function executeApplyStatusAction(action, context) { return { success: true, consumed: true }; } +function getStatusEffectConfig(statusId) { + const statusEffects = CONFIG.statusEffects ?? []; + if (Array.isArray(statusEffects)) return statusEffects.find(status => status.id === statusId); + if (statusEffects instanceof Map) return statusEffects.get(statusId) ?? null; + return Object.values(statusEffects).find(status => status.id === statusId) ?? null; +} + function buildEffectDuration(duration) { if (!duration || duration.type === "unlimited") return {}; diff --git a/scripts/apps/assignment-manager.js b/scripts/apps/assignment-manager.js index 29d2325..dd6146c 100644 --- a/scripts/apps/assignment-manager.js +++ b/scripts/apps/assignment-manager.js @@ -57,6 +57,7 @@ export async function assignReactionToSelectedTokens(reactionId) { await ensureManagedReactionEffectOnActor(actor, reaction); } else { await addReactionFlag(tokenDocument, reactionId); + await ensureManagedReactionEffectOnActor(actor, reaction, tokenDocument.uuid); } assignedCount++; @@ -82,7 +83,10 @@ export async function removeAssignment(assignmentId) { if (assignment.mode === "token" && assignment.tokenUuid) { const tokenDocument = await fromUuid(assignment.tokenUuid); - if (tokenDocument) await removeReactionFromDocument(tokenDocument, assignment.reactionId, false); + if (tokenDocument) { + await removeReactionFromDocument(tokenDocument, assignment.reactionId, false); + if (tokenDocument.actor) await removeReactionFromDocument(tokenDocument.actor, assignment.reactionId, true, tokenDocument.uuid); + } } } @@ -118,12 +122,13 @@ async function addReactionFlag(document, reactionId) { await document.setFlag(MODULE_ID, "assignedReactions", assigned); } -async function ensureManagedReactionEffectOnActor(actor, reaction) { +async function ensureManagedReactionEffectOnActor(actor, reaction, tokenUuid = null) { if (!game.settings.get(MODULE_ID, SETTINGS.CREATE_MANAGED_EFFECTS_ON_ASSIGNMENT)) return; const existing = actor.effects.find(effect => effect.getFlag(MODULE_ID, "reactionId") === reaction.id && - effect.getFlag(MODULE_ID, "managed") === true + effect.getFlag(MODULE_ID, "managed") === true && + (tokenUuid === null || effect.getFlag(MODULE_ID, "tokenUuid") === tokenUuid) ); if (existing) return; @@ -138,13 +143,14 @@ async function ensureManagedReactionEffectOnActor(actor, reaction) { flags: { [MODULE_ID]: { reactionId: reaction.id, - managed: true + managed: true, + tokenUuid } } }]); } -async function removeReactionFromDocument(document, reactionId, removeManagedEffects) { +async function removeReactionFromDocument(document, reactionId, removeManagedEffects, tokenUuid = null) { const assigned = foundry.utils.deepClone(document.getFlag(MODULE_ID, "assignedReactions") ?? []) .filter(entry => entry.reactionId !== reactionId); @@ -154,7 +160,11 @@ async function removeReactionFromDocument(document, reactionId, removeManagedEff if (!removeManagedEffects || !document.effects) return; const effectIds = document.effects - .filter(effect => effect.getFlag(MODULE_ID, "reactionId") === reactionId && effect.getFlag(MODULE_ID, "managed") === true) + .filter(effect => + effect.getFlag(MODULE_ID, "reactionId") === reactionId && + effect.getFlag(MODULE_ID, "managed") === true && + (tokenUuid === null || effect.getFlag(MODULE_ID, "tokenUuid") === tokenUuid) + ) .map(effect => effect.id); if (effectIds.length) await document.deleteEmbeddedDocuments("ActiveEffect", effectIds); diff --git a/scripts/conditions/damage-condition.js b/scripts/conditions/damage-condition.js index 8df6f31..e81333b 100644 --- a/scripts/conditions/damage-condition.js +++ b/scripts/conditions/damage-condition.js @@ -13,9 +13,21 @@ export async function checkDamageCondition(config, context) { if (!configuredTypes.length) return true; const eventTypes = new Set(context.damageTypes ?? []); - if (!eventTypes.size) return false; + if (!eventTypes.size) return configuredTypesCoverAllKnownTypes(configuredTypes); if (config.typeMode === "all") return configuredTypes.every(type => eventTypes.has(type)); if (config.typeMode === "none") return configuredTypes.every(type => !eventTypes.has(type)); return configuredTypes.some(type => eventTypes.has(type)); } + +function configuredTypesCoverAllKnownTypes(configuredTypes) { + const knownTypes = getKnownDamageTypes(); + if (!knownTypes.size) return false; + return Array.from(knownTypes).every(type => configuredTypes.includes(type)); +} + +function getKnownDamageTypes() { + const damageTypes = CONFIG.DND5E?.damageTypes ?? {}; + if (damageTypes instanceof Map) return new Set(damageTypes.keys()); + return new Set(Object.keys(damageTypes)); +} diff --git a/scripts/triggers/damage-received.js b/scripts/triggers/damage-received.js index 5ac415d..4800d19 100644 --- a/scripts/triggers/damage-received.js +++ b/scripts/triggers/damage-received.js @@ -7,31 +7,75 @@ export function registerDnd5eDamageTrigger() { if (!game.user.isGM) return; if (!actor) return; - const tokenDocument = options.token ?? options.tokenDocument ?? getBestTokenDocumentForActor(actor); - const damageTypes = extractDamageTypes(options); + const hookOptions = normalizeDamageHookOptions(options); + const tokenDocument = resolveDamageTokenDocument(actor, hookOptions); + const damageTypes = extractDamageTypes(hookOptions); + const numericAmount = Number(amount) || 0; await handleTrigger(TRIGGER_TYPES.DAMAGE_RECEIVED, { - actor, - targetActor: actor, + actor: tokenDocument?.actor ?? actor, + targetActor: tokenDocument?.actor ?? actor, tokenDocument, targetTokenDocument: tokenDocument, targetToken: tokenDocument?.object ?? null, - amount: Number(amount) || 0, - damageAmount: Number(amount) || 0, + amount: numericAmount, + damageAmount: numericAmount, damageTypes, - options + options: hookOptions }); }); } -function extractDamageTypes(options) { - const candidates = [ - options.damageTypes, - options.types, - options.damage?.types, - options.workflow?.damageItem?.damageDetail?.map?.(part => part.type) - ]; - - const values = candidates.flat().filter(Boolean); - return [...new Set(values)]; +function normalizeDamageHookOptions(options) { + return options && typeof options === "object" ? options : {}; +} + +function resolveDamageTokenDocument(actor, options) { + return options.tokenDocument ?? + options.token?.document ?? + options.token ?? + options.targetTokenDocument ?? + options.targetToken?.document ?? + actor.token ?? + getBestTokenDocumentForActor(actor); +} + +function extractDamageTypes(options) { + const knownTypes = getKnownDamageTypes(); + const collected = new Set(); + collectDamageTypes(options, knownTypes, collected); + return Array.from(collected); +} + +function collectDamageTypes(value, knownTypes, collected, seen = new WeakSet(), depth = 0) { + if (value == null || depth > 5) return; + + if (typeof value === "string") { + if (knownTypes.has(value)) collected.add(value); + return; + } + + if (typeof value !== "object") return; + if (seen.has(value)) return; + seen.add(value); + + if (Array.isArray(value) || value instanceof Set) { + for (const entry of value) collectDamageTypes(entry, knownTypes, collected, seen, depth + 1); + return; + } + + if (value instanceof Map) { + for (const entry of value.values()) collectDamageTypes(entry, knownTypes, collected, seen, depth + 1); + return; + } + + for (const key of ["type", "damageType", "damageTypeId", "types", "damageTypes", "damage", "damages", "damageDetail", "parts"]) { + collectDamageTypes(value[key], knownTypes, collected, seen, depth + 1); + } +} + +function getKnownDamageTypes() { + const damageTypes = CONFIG.DND5E?.damageTypes ?? {}; + if (damageTypes instanceof Map) return new Set(damageTypes.keys()); + return new Set(Object.keys(damageTypes)); } diff --git a/scripts/utils/token-utils.js b/scripts/utils/token-utils.js index 1ceef14..a94449c 100644 --- a/scripts/utils/token-utils.js +++ b/scripts/utils/token-utils.js @@ -1,18 +1,27 @@ export function getBestTokenDocumentForActor(actor) { if (!actor) return null; + const syntheticToken = getSyntheticTokenDocument(actor); + if (syntheticToken) return syntheticToken; + const controlled = canvas?.tokens?.controlled?.find(token => token.actor?.uuid === actor.uuid || token.actor?.id === actor.id); if (controlled) return controlled.document; - const activeTokens = actor.getActiveTokens?.(true, true) ?? []; - if (activeTokens.length === 1) return activeTokens[0].document; - const combatant = game.combat?.combatants?.find(c => c.actor?.uuid === actor.uuid || c.actor?.id === actor.id); if (combatant?.token) return combatant.token; + const activeTokens = actor.getActiveTokens?.(true, true) ?? []; + if (activeTokens.length === 1) return activeTokens[0].document; + return activeTokens[0]?.document ?? null; } +function getSyntheticTokenDocument(actor) { + const tokenDocument = actor.token ?? actor.prototypeToken ?? null; + if (tokenDocument?.documentName === "Token") return tokenDocument; + return null; +} + export function getPrimaryOwner(actor) { if (!actor) return null; return game.users.find(user => user.active && !user.isGM && actor.testUserPermission(user, "OWNER")) ?? null;