import { MODULE_ID, SETTINGS } from "../constants.js"; import { emitSocket } from "../sockets.js"; import { getPrimaryOwner, isTokenSpaceOccupied } from "../utils/token-utils.js"; import { isWithinTeleportRadius, randomPointInCircle, sceneDistanceToPixels } from "../utils/distance-utils.js"; export async function executeTeleportAction(action, context) { const tokenDocument = context.targetTokenDocument ?? context.tokenDocument; if (!tokenDocument) return { success: false, consumed: false, reason: "NO_TOKEN" }; if (action.askOwner === true) { return requestOwnerTeleportTarget({ tokenDocument, action, context }); } return executeRandomTeleport({ tokenDocument, action }); } async function requestOwnerTeleportTarget({ tokenDocument, action }) { const actor = tokenDocument.actor; const owner = getPrimaryOwner(actor); if (!owner) { return executeRandomTeleport({ tokenDocument, action }); } emitSocket({ type: "requestTeleportTarget", userId: owner.id, sceneId: tokenDocument.parent.id, tokenId: tokenDocument.id, radius: Number(action.radius) || 30, snapToGrid: action.snapToGrid !== false }); return { success: true, consumed: true, pending: true }; } export async function handleTeleportTargetRequest(message) { if (message.userId !== game.user.id) return; const scene = game.scenes.get(message.sceneId); if (!scene || canvas.scene?.id !== scene.id) { ui.notifications.warn(game.i18n.localize("CONFIGURABLE_REACTIONS.Teleport.SwitchScene")); return; } const token = canvas.tokens.get(message.tokenId); if (!token) return; const confirmed = await foundry.applications.api.DialogV2.confirm({ window: { title: game.i18n.localize("CONFIGURABLE_REACTIONS.Teleport.Title") }, content: `

${game.i18n.format("CONFIGURABLE_REACTIONS.Teleport.ChooseTarget", { radius: message.radius })}

`, yes: { label: game.i18n.localize("CONFIGURABLE_REACTIONS.Common.Choose") }, no: { label: game.i18n.localize("CONFIGURABLE_REACTIONS.Common.Cancel") } }); if (!confirmed) return; const point = await waitForCanvasClick(); emitSocket({ type: "teleportTargetChosen", sceneId: message.sceneId, tokenId: message.tokenId, userId: game.user.id, x: point.x, y: point.y, radius: message.radius, snapToGrid: message.snapToGrid !== false }); } export async function handleTeleportTargetChosen(message) { if (!game.user.isGM) return; const scene = game.scenes.get(message.sceneId); const tokenDocument = scene?.tokens.get(message.tokenId); const token = tokenDocument?.object; if (!tokenDocument || !token) return; const user = game.users.get(message.userId); if (!tokenDocument.actor?.testUserPermission(user, "OWNER")) return; let position = centerToTopLeft(token, { x: message.x, y: message.y }); if (message.snapToGrid !== false) position = snapPosition(token, position); const radiusPx = sceneDistanceToPixels(Number(message.radius) || 30); if (!isTeleportCandidateValid(token, position, radiusPx)) { ui.notifications.warn(game.i18n.localize("CONFIGURABLE_REACTIONS.Teleport.InvalidTarget")); return; } await tokenDocument.update({ x: position.x, y: position.y }); } export async function executeRandomTeleport({ tokenDocument, action }) { const token = tokenDocument.object; if (!token) return { success: false, consumed: false, reason: "NO_TOKEN_OBJECT" }; const target = findRandomReachableTeleportTarget(token, { radius: Number(action.radius) || 30, maxAttempts: Number(action.maxAttempts) || 80, snapToGrid: action.snapToGrid !== false }); if (!target) { ui.notifications.warn(game.i18n.format("CONFIGURABLE_REACTIONS.Teleport.NoValidTarget", { name: token.name, radius: Number(action.radius) || 30 })); await createTeleportFailureMessage(token, action); return { success: false, consumed: action.consumeOnFailure === true, reason: "NO_VALID_TELEPORT_TARGET" }; } await tokenDocument.update({ x: target.x, y: target.y }); return { success: true, consumed: true }; } function findRandomReachableTeleportTarget(token, options) { const radiusPx = sceneDistanceToPixels(options.radius); const originCenter = token.center; for (let attempt = 0; attempt < options.maxAttempts; attempt++) { const candidateCenter = randomPointInCircle(originCenter, radiusPx); let targetPosition = centerToTopLeft(token, candidateCenter); if (options.snapToGrid) targetPosition = snapPosition(token, targetPosition); if (isTeleportCandidateValid(token, targetPosition, radiusPx)) return targetPosition; } return null; } function isTeleportCandidateValid(token, targetPosition, radiusPx) { const candidateCenter = token.getCenterPoint(targetPosition); if (!isWithinTeleportRadius(token.center, candidateCenter, radiusPx)) return false; if (!isWithinSceneBounds(token, targetPosition)) return false; if (movementBlockedByWalls(token, candidateCenter)) return false; if (isTokenSpaceOccupied(token, targetPosition)) return false; return true; } function movementBlockedByWalls(token, destinationCenter) { const origin = token.document.getMovementOrigin?.() ?? token.center; const destination = { x: destinationCenter.x, y: destinationCenter.y, elevation: token.document.elevation }; return token.checkCollision(destination, { origin, type: "move", mode: "any" }) === true; } function isWithinSceneBounds(token, position) { const dimensions = canvas.scene.dimensions; return position.x >= 0 && position.y >= 0 && position.x + token.w <= dimensions.width && position.y + token.h <= dimensions.height; } function centerToTopLeft(token, center) { return { x: center.x - token.w / 2, y: center.y - token.h / 2 }; } function snapPosition(token, position) { if (typeof token.getSnappedPosition === "function") return token.getSnappedPosition(position); const snapped = canvas.grid.getSnappedPoint(position); return { x: snapped.x, y: snapped.y }; } function waitForCanvasClick() { return new Promise(resolve => { const handler = event => { canvas.stage.off("pointerdown", handler); const position = event.data.getLocalPosition(canvas.stage); resolve({ x: position.x, y: position.y }); }; canvas.stage.on("pointerdown", handler); }); } async function createTeleportFailureMessage(token, action) { if (!game.settings.get(MODULE_ID, SETTINGS.SHOW_FAILED_TELEPORT_MESSAGES)) return; await ChatMessage.create({ speaker: ChatMessage.getSpeaker({ token }), content: `

${token.name} findet kein erreichbares freies Teleportziel innerhalb von ${Number(action.radius) || 30} ft.

` }); }