207 lines
6.6 KiB
JavaScript
207 lines
6.6 KiB
JavaScript
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: `<p>${game.i18n.format("CONFIGURABLE_REACTIONS.Teleport.ChooseTarget", { radius: message.radius })}</p>`,
|
|
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: `<p><strong>${token.name}</strong> findet kein erreichbares freies Teleportziel innerhalb von ${Number(action.radius) || 30} ft.</p>`
|
|
});
|
|
}
|