import { TRIGGER_TYPES } from "../constants.js"; import { handleTrigger } from "../engine/reaction-engine.js"; import { getBestTokenDocumentForActor } from "../utils/token-utils.js"; export function registerDnd5eDamageTrigger() { Hooks.on("dnd5e.applyDamage", async (actor, amount, options = {}) => { await handleDamageReceivedHook(actor, amount, options); }); Hooks.on("dnd5e.damageActor", async (actor, changes = {}, update = {}, userId = null) => { const amount = normalizeDamageActorAmount(changes, update); await handleDamageReceivedHook(actor, amount, { changes, update, userId }); }); } async function handleDamageReceivedHook(actor, amount, options = {}) { if (!game.user.isGM || !actor) return; const hookOptions = normalizeDamageHookOptions(options); const tokenDocument = resolveDamageTokenDocument(actor, hookOptions); const targetActor = tokenDocument?.actor ?? actor; const numericAmount = Number(amount) || 0; await handleTrigger(TRIGGER_TYPES.DAMAGE_RECEIVED, { actor: targetActor, targetActor, tokenDocument, targetTokenDocument: tokenDocument, targetToken: tokenDocument?.object ?? null, amount: numericAmount, damageAmount: numericAmount, damageTypes: extractDamageTypes(hookOptions), options: hookOptions }); } function normalizeDamageHookOptions(options) { return options && typeof options === "object" ? options : {}; } function normalizeDamageActorAmount(changes, update) { const candidates = [changes?.total, changes?.amount, changes?.value, update?.total, update?.amount, update?.value]; const value = candidates.find(candidate => Number.isFinite(Number(candidate))); return Number(value) || 0; } function resolveDamageTokenDocument(actor, options) { return asTokenDocument(options.tokenDocument) ?? asTokenDocument(options.token) ?? asTokenDocument(options.targetTokenDocument) ?? asTokenDocument(options.targetToken) ?? asTokenDocument(actor.token) ?? getBestTokenDocumentForActor(actor); } function asTokenDocument(value) { if (!value) return null; if (value.documentName === "Token") return value; if (value.document?.documentName === "Token") return value.document; return null; } 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" || 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", "workflow"]) { 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)); }