import { MODULE_ID, SETTINGS } from "../constants.js"; const ASSIGNED_REACTIONS_FLAG = "assignedReactions"; const MANAGED_FEATURE_FLAG = "managedAssignmentFeature"; const FEATURE_TYPE = "feat"; export async function assignReactionToSelectedTokens(reactionId) { if (!game.user.isGM) { ui.notifications.warn(game.i18n.localize("CONFIGURABLE_REACTIONS.Errors.GmOnly")); return; } if (!reactionId) { ui.notifications.warn(game.i18n.localize("CONFIGURABLE_REACTIONS.Errors.NoReactionSelected")); return; } const selectedTokens = canvas?.tokens?.controlled ?? []; if (!selectedTokens.length) { ui.notifications.warn(game.i18n.localize("CONFIGURABLE_REACTIONS.Errors.NoTokensSelected")); return; } const reactions = game.settings.get(MODULE_ID, SETTINGS.REACTIONS) ?? []; const reaction = reactions.find(candidate => candidate.id === reactionId); if (!reaction) { ui.notifications.error(game.i18n.localize("CONFIGURABLE_REACTIONS.Errors.ReactionNotFound")); return; } const assignments = foundry.utils.deepClone(game.settings.get(MODULE_ID, SETTINGS.ASSIGNMENTS) ?? []); let assignedCount = 0; let skippedCount = 0; for (const token of selectedTokens) { const result = await assignReactionToToken({ token, reaction, reactionId, assignments }); if (result === "assigned") assignedCount++; else skippedCount++; } await game.settings.set(MODULE_ID, SETTINGS.ASSIGNMENTS, assignments); ui.notifications.info(game.i18n.format("CONFIGURABLE_REACTIONS.Assignments.AssignedResult", { assigned: assignedCount, skipped: skippedCount })); } async function assignReactionToToken({ token, reaction, reactionId, assignments }) { const tokenDocument = token?.document; const actor = token?.actor; if (!tokenDocument || !actor) return "skipped"; const assignment = buildAssignmentForToken(tokenDocument, actor, reactionId); if (hasAssignment(assignments, assignment)) return "skipped"; assignments.push(assignment); if (assignment.mode === "actor") { await addReactionFlag(actor, reactionId); await ensureManagedReactionFeature(actor, reaction); return "assigned"; } await addReactionFlag(tokenDocument, reactionId); await ensureManagedReactionFeature(actor, reaction, tokenDocument.uuid); return "assigned"; } function hasAssignment(assignments, assignment) { return assignments.some(existing => existing.reactionId === assignment.reactionId && existing.mode === assignment.mode && existing.actorUuid === assignment.actorUuid && existing.tokenUuid === assignment.tokenUuid ); } export async function removeAssignment(assignmentId) { if (!game.user.isGM) return; const assignments = foundry.utils.deepClone(game.settings.get(MODULE_ID, SETTINGS.ASSIGNMENTS) ?? []); const assignment = assignments.find(candidate => candidate.id === assignmentId); if (!assignment) return; await game.settings.set(MODULE_ID, SETTINGS.ASSIGNMENTS, assignments.filter(candidate => candidate.id !== assignmentId)); await removeAssignmentMarkers(assignment); } async function removeAssignmentMarkers(assignment) { if (assignment.mode === "actor" && assignment.actorUuid) { const actor = await fromUuid(assignment.actorUuid); if (actor) await removeReactionFromDocument(actor, assignment.reactionId, true); return; } if (assignment.mode !== "token" || !assignment.tokenUuid) return; const tokenDocument = await fromUuid(assignment.tokenUuid); if (!tokenDocument) return; await removeReactionFromDocument(tokenDocument, assignment.reactionId, false); if (tokenDocument.actor) await removeReactionFromDocument(tokenDocument.actor, assignment.reactionId, true, tokenDocument.uuid); } function buildAssignmentForToken(tokenDocument, actor, reactionId) { if (tokenDocument.actorLink === true) { return { id: foundry.utils.randomID(), reactionId, mode: "actor", actorUuid: actor.uuid, tokenUuid: null, sceneUuid: null, name: actor.name }; } return { id: foundry.utils.randomID(), reactionId, mode: "token", actorUuid: null, tokenUuid: tokenDocument.uuid, sceneUuid: tokenDocument.parent?.uuid ?? canvas.scene?.uuid ?? null, name: tokenDocument.name }; } async function addReactionFlag(document, reactionId) { const assigned = foundry.utils.deepClone(document.getFlag(MODULE_ID, ASSIGNED_REACTIONS_FLAG) ?? []); if (!assigned.some(entry => entry.reactionId === reactionId)) { assigned.push({ reactionId, assignedAt: Date.now(), assignedBy: game.user.id }); } await document.setFlag(MODULE_ID, ASSIGNED_REACTIONS_FLAG, assigned); } async function ensureManagedReactionFeature(actor, reaction, tokenUuid = null) { if (!game.settings.get(MODULE_ID, SETTINGS.CREATE_MANAGED_EFFECTS_ON_ASSIGNMENT)) return; if (!actor?.createEmbeddedDocuments) return; if (findManagedReactionFeature(actor, reaction.id, tokenUuid)) return; await actor.createEmbeddedDocuments("Item", [buildManagedReactionFeatureData(reaction, tokenUuid)]); } function findManagedReactionFeature(actor, reactionId, tokenUuid = null) { return actor.items?.find(item => item.type === FEATURE_TYPE && item.getFlag(MODULE_ID, "reactionId") === reactionId && item.getFlag(MODULE_ID, MANAGED_FEATURE_FLAG) === true && (tokenUuid === null || item.getFlag(MODULE_ID, "tokenUuid") === tokenUuid) ); } function buildManagedReactionFeatureData(reaction, tokenUuid = null) { const name = game.i18n.format("CONFIGURABLE_REACTIONS.Effects.ManagedReaction", { name: reaction.name }); return { name, type: FEATURE_TYPE, img: reaction.icon ?? "icons/svg/aura.svg", system: { description: { value: `

${name}

`, chat: "" } }, flags: { [MODULE_ID]: { reactionId: reaction.id, managed: true, [MANAGED_FEATURE_FLAG]: true, tokenUuid } } }; } async function removeReactionFromDocument(document, reactionId, removeManagedFeature, tokenUuid = null) { const assigned = foundry.utils.deepClone(document.getFlag(MODULE_ID, ASSIGNED_REACTIONS_FLAG) ?? []) .filter(entry => entry.reactionId !== reactionId); if (assigned.length) await document.setFlag(MODULE_ID, ASSIGNED_REACTIONS_FLAG, assigned); else await document.unsetFlag(MODULE_ID, ASSIGNED_REACTIONS_FLAG); if (removeManagedFeature) await removeManagedReactionFeatures(document, reactionId, tokenUuid); } async function removeManagedReactionFeatures(actor, reactionId, tokenUuid = null) { const itemIds = actor.items ?.filter(item => item.getFlag(MODULE_ID, "reactionId") === reactionId && item.getFlag(MODULE_ID, MANAGED_FEATURE_FLAG) === true && (tokenUuid === null || item.getFlag(MODULE_ID, "tokenUuid") === tokenUuid) ) .map(item => item.id) ?? []; if (itemIds.length) await actor.deleteEmbeddedDocuments("Item", itemIds); }