439 lines
15 KiB
JavaScript
439 lines
15 KiB
JavaScript
import {highlightMeasurementTerrainRuler, measureDistances} from "./compatibility.js";
|
|
import {
|
|
getGridPositionFromPixels,
|
|
getGridPositionFromPixelsObj,
|
|
getPixelsFromGridPositionObj,
|
|
} from "./foundry_fixes.js";
|
|
import {Line} from "./geometry.js";
|
|
import {disableSnap, moveWithoutAnimation} from "./keybindings.js";
|
|
import {trackRays} from "./movement_tracking.js";
|
|
import {findPath, isPathfindingEnabled} from "./pathfinding.js";
|
|
import {settingsKey} from "./settings.js";
|
|
import {recalculate} from "./socket.js";
|
|
import {
|
|
applyTokenSizeOffset,
|
|
enumeratedZip,
|
|
getSnapPointForEntity,
|
|
getSnapPointForToken,
|
|
getSnapPointForTokenObj,
|
|
getTokenShape,
|
|
highlightTokenShape,
|
|
sum,
|
|
} from "./util.js";
|
|
|
|
// This is a modified version of Ruler.moveToken from foundry 0.7.9
|
|
export async function moveEntities(draggedEntity, selectedEntities) {
|
|
let wasPaused = game.paused;
|
|
if (wasPaused && !game.user.isGM) {
|
|
ui.notifications.warn(game.i18n.localize("GAME.PausedWarning"));
|
|
return false;
|
|
}
|
|
if (!this.visible || !this.destination) return false;
|
|
if (!draggedEntity) return;
|
|
|
|
// Wait until all scheduled measurements are done
|
|
await this.deferredMeasurementPromise;
|
|
|
|
// Get the movement rays and check collision along each Ray
|
|
// These rays are center-to-center for the purposes of collision checking
|
|
const rays = this.constructor.dragRulerGetRaysFromWaypoints(this.waypoints, this.destination);
|
|
if (!game.user.isGM && draggedEntity instanceof Token) {
|
|
const hasCollision = selectedEntities.some(token => {
|
|
const offset = calculateEntityOffset(token, draggedEntity);
|
|
const offsetRays = rays
|
|
.filter(ray => !ray.isPrevious)
|
|
.map(ray => applyOffsetToRay(ray, offset));
|
|
if (window.WallHeight) {
|
|
window.WallHeight.addBoundsToRays(offsetRays, draggedEntity);
|
|
}
|
|
return offsetRays.some(r => canvas.walls.checkCollision(r));
|
|
});
|
|
if (hasCollision) {
|
|
ui.notifications.error(game.i18n.localize("ERROR.TokenCollide"));
|
|
this._endMeasurement();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Execute the movement path.
|
|
// Transform each center-to-center ray into a top-left to top-left ray using the prior token offsets.
|
|
this._state = Ruler.STATES.MOVING;
|
|
await animateEntities.call(this, selectedEntities, draggedEntity, rays, wasPaused);
|
|
|
|
// Once all animations are complete we can clear the ruler
|
|
if (this.draggedEntity?.id === draggedEntity.id) this._endMeasurement();
|
|
}
|
|
|
|
// This is a modified version code extracted from Ruler.moveToken from foundry 0.7.9
|
|
async function animateEntities(entities, draggedEntity, draggedRays, wasPaused) {
|
|
const newRays = draggedRays.filter(r => !r.isPrevious);
|
|
const entityAnimationData = entities.map(entity => {
|
|
const entityOffset = calculateEntityOffset(entity, draggedEntity);
|
|
const offsetRays = newRays.map(ray => applyOffsetToRay(ray, entityOffset));
|
|
|
|
// Determine offset relative to the Token top-left.
|
|
// This is important so we can position the token relative to the ruler origin for non-1x1 tokens.
|
|
const firstWaypoint = this.waypoints.find(w => !w.isPrevious);
|
|
const origin = [firstWaypoint.x + entityOffset.x, firstWaypoint.y + entityOffset.y];
|
|
let dx, dy;
|
|
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
|
dx = entity.data.x - origin[0];
|
|
dy = entity.data.y - origin[1];
|
|
} else {
|
|
dx = entity.data.x - origin[0];
|
|
dy = entity.data.y - origin[1];
|
|
}
|
|
|
|
return {entity, rays: offsetRays, dx, dy};
|
|
});
|
|
|
|
const isToken = draggedEntity instanceof Token;
|
|
const animate = isToken && !moveWithoutAnimation;
|
|
const startWaypoint = animate ? 0 : entityAnimationData[0].rays.length - 1;
|
|
|
|
// This is a flag of the "Monk's Active Tile Triggers" module that signals that the movement should be cancelled early
|
|
this.cancelMovement = false;
|
|
|
|
for (let i = startWaypoint; i < entityAnimationData[0].rays.length; i++) {
|
|
if (!wasPaused && game.paused) break;
|
|
const entityPaths = entityAnimationData.map(({entity, rays, dx, dy}) => {
|
|
const ray = rays[i];
|
|
const dest = [ray.B.x, ray.B.y];
|
|
const path = new Ray({x: entity.x, y: entity.y}, {x: dest[0] + dx, y: dest[1] + dy});
|
|
return {entity, path};
|
|
});
|
|
const updates = entityPaths.map(({entity, path}) => {
|
|
return {x: path.B.x, y: path.B.y, _id: entity.id};
|
|
});
|
|
await draggedEntity.scene.updateEmbeddedDocuments(
|
|
draggedEntity.constructor.embeddedName,
|
|
updates,
|
|
{animate},
|
|
);
|
|
if (animate)
|
|
await Promise.all(
|
|
entityPaths.map(
|
|
({entity}) => CanvasAnimation.getAnimation(entity.movementAnimationName)?.promise,
|
|
),
|
|
);
|
|
|
|
// This is a flag of the "Monk's Active Tile Triggers" module that signals that the movement should be cancelled early
|
|
if (this.cancelMovement) {
|
|
entityAnimationData.forEach(ead => (ead.rays = ead.rays.slice(0, i + 1)));
|
|
break;
|
|
}
|
|
}
|
|
if (isToken)
|
|
trackRays(
|
|
entities,
|
|
entityAnimationData.map(({rays}) => rays),
|
|
).then(() => recalculate(entities));
|
|
}
|
|
|
|
function calculateEntityOffset(entityA, entityB) {
|
|
return {x: entityA.data.x - entityB.data.x, y: entityA.data.y - entityB.data.y};
|
|
}
|
|
|
|
function applyOffsetToRay(ray, offset) {
|
|
const newRay = new Ray(
|
|
{x: ray.A.x + offset.x, y: ray.A.y + offset.y},
|
|
{x: ray.B.x + offset.x, y: ray.B.y + offset.y},
|
|
);
|
|
newRay.isPrevious = ray.isPrevious;
|
|
return newRay;
|
|
}
|
|
|
|
// This is a modified version of Ruler._onMouseMove from foundry 0.7.9
|
|
export function onMouseMove(event) {
|
|
if (this._state === Ruler.STATES.MOVING) return;
|
|
|
|
// Extract event data
|
|
const destination = {
|
|
x: event.data.destination.x + this.rulerOffset.x,
|
|
y: event.data.destination.y + this.rulerOffset.y,
|
|
};
|
|
|
|
// Hide any existing Token HUD
|
|
canvas.hud.token.clear();
|
|
delete event.data.hudState;
|
|
|
|
// Draw measurement updates
|
|
scheduleMeasurement.call(this, destination, event);
|
|
}
|
|
|
|
function scheduleMeasurement(destination, event) {
|
|
const measurementInterval = 50;
|
|
const mt = event._measureTime || 0;
|
|
const originalEvent = event.data.originalEvent;
|
|
if (Date.now() - mt > measurementInterval) {
|
|
this.measure(destination, {snap: !disableSnap});
|
|
event._measureTime = Date.now();
|
|
this._state = Ruler.STATES.MEASURING;
|
|
cancelScheduledMeasurement.call(this);
|
|
} else {
|
|
this.deferredMeasurementData = {destination, event};
|
|
if (!this.deferredMeasurementTimeout) {
|
|
this.deferredMeasurementPromise = new Promise(
|
|
(resolve, reject) => (this.deferredMeasurementResolve = resolve),
|
|
);
|
|
this.deferredMeasurementTimeout = window.setTimeout(
|
|
() =>
|
|
scheduleMeasurement.call(
|
|
this,
|
|
this.deferredMeasurementData.destination,
|
|
this.deferredMeasurementData.event,
|
|
),
|
|
measurementInterval,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function cancelScheduledMeasurement() {
|
|
window.clearTimeout(this.deferredMeasurementTimeout);
|
|
this.deferredMeasurementTimeout = undefined;
|
|
this.deferredMeasurementResolve?.();
|
|
}
|
|
|
|
// This is a modified version of Ruler.measure form foundry 0.7.9
|
|
export function measure(destination, options = {}) {
|
|
const isToken = this.draggedEntity instanceof Token;
|
|
if (isToken && !this.draggedEntity.isVisible) return [];
|
|
|
|
options.snap = options.snap ?? !disableSnap;
|
|
|
|
if (options.snap) {
|
|
destination = getSnapPointForEntity(destination.x, destination.y, this.draggedEntity);
|
|
}
|
|
|
|
this.dragRulerRemovePathfindingWaypoints();
|
|
|
|
if (isToken && isPathfindingEnabled.call(this)) {
|
|
const from = getGridPositionFromPixelsObj(this.waypoints[this.waypoints.length - 1]);
|
|
const to = getGridPositionFromPixelsObj(destination);
|
|
let path = findPath(from, to, this.draggedEntity, this.waypoints);
|
|
if (path) path.shift();
|
|
if (path && path.length > 0) {
|
|
path = path.map(point =>
|
|
getSnapPointForTokenObj(getPixelsFromGridPositionObj(point), this.draggedEntity),
|
|
);
|
|
|
|
// If the token is snapped to the grid, the first point of the path is already handled by the ruler
|
|
if (
|
|
path[0].x === this.waypoints[this.waypoints.length - 1].x &&
|
|
path[0].y === this.waypoints[this.waypoints.length - 1].y
|
|
)
|
|
path = path.slice(1);
|
|
|
|
// If snapping is enabled, the last point of the path is already handled by the ruler
|
|
if (options.snap) path = path.slice(0, path.length - 1);
|
|
|
|
for (const point of path) {
|
|
point.isPathfinding = true;
|
|
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
|
|
}
|
|
this.waypoints = this.waypoints.concat(path);
|
|
} else {
|
|
// Don't show a path if the pathfinding yields no result to show the user that the destination is unreachable
|
|
destination = this.waypoints[this.waypoints.length - 1];
|
|
}
|
|
}
|
|
|
|
if (options.gridSpaces === undefined) {
|
|
options.gridSpaces = canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS;
|
|
}
|
|
|
|
if (canvas.grid.diagonalRule === "EUCL") {
|
|
options.gridSpaces = false;
|
|
options.ignoreGrid = true;
|
|
}
|
|
|
|
if (options.ignoreGrid === undefined) {
|
|
options.ignoreGrid = false;
|
|
}
|
|
|
|
options.enableTerrainRuler = isToken && game.modules.get("terrain-ruler")?.active;
|
|
|
|
const waypoints = this.waypoints.concat([destination]);
|
|
// Move the waypoints to the center of the grid if a size is used that measures from edge to edge
|
|
const centeredWaypoints = isToken
|
|
? applyTokenSizeOffset(waypoints, this.draggedEntity)
|
|
: duplicate(waypoints);
|
|
// Foundries native ruler requires the waypoints to sit in the dead center of the square to work properly
|
|
if (!options.enableTerrainRuler && !options.ignoreGrid)
|
|
centeredWaypoints.forEach(w => ([w.x, w.y] = canvas.grid.getCenter(w.x, w.y)));
|
|
|
|
const r = this.ruler;
|
|
this.destination = destination;
|
|
|
|
// Iterate over waypoints and construct segment rays
|
|
const segments = [];
|
|
const centeredSegments = [];
|
|
for (let [i, dest] of waypoints.slice(1).entries()) {
|
|
const centeredDest = centeredWaypoints[i + 1];
|
|
const origin = waypoints[i];
|
|
const centeredOrigin = centeredWaypoints[i];
|
|
const label = this.labels.children[i];
|
|
const ray = new Ray(origin, dest);
|
|
const centeredRay = new Ray(centeredOrigin, centeredDest);
|
|
ray.isPrevious = Boolean(origin.isPrevious);
|
|
centeredRay.isPrevious = ray.isPrevious;
|
|
ray.dragRulerVisitedSpaces = origin.dragRulerVisitedSpaces;
|
|
centeredRay.dragRulerVisitedSpaces = ray.dragRulerVisitedSpaces;
|
|
ray.dragRulerFinalState = origin.dragRulerFinalState;
|
|
centeredRay.dragRulerFinalState = ray.dragRulerFinalState;
|
|
if (ray.distance < 10) {
|
|
if (label) label.visible = false;
|
|
continue;
|
|
}
|
|
segments.push({ray, label});
|
|
centeredSegments.push({ray: centeredRay, label});
|
|
}
|
|
|
|
const shape = isToken ? getTokenShape(this.draggedEntity) : null;
|
|
|
|
// Compute measured distance
|
|
const distances = measureDistances(centeredSegments, this.draggedEntity, shape, options);
|
|
|
|
let totalDistance = 0;
|
|
for (let [i, d] of distances.entries()) {
|
|
let s = centeredSegments[i];
|
|
s.startDistance = totalDistance;
|
|
totalDistance += d;
|
|
s.last = i === centeredSegments.length - 1;
|
|
s.distance = d;
|
|
s.text = this._getSegmentLabel(d, totalDistance, s.last);
|
|
}
|
|
|
|
// Clear the grid highlight layer
|
|
const hlt = canvas.grid.highlightLayers[this.name] || canvas.grid.addHighlightLayer(this.name);
|
|
hlt.clear();
|
|
|
|
// Draw measured path
|
|
r.clear();
|
|
let rulerColor;
|
|
if (!options.gridSpaces || canvas.grid.type === CONST.GRID_TYPES.GRIDLESS)
|
|
rulerColor = this.dragRulerGetColorForDistance(totalDistance);
|
|
else rulerColor = this.color;
|
|
for (const [i, s, cs] of enumeratedZip(
|
|
[...segments].reverse(),
|
|
[...centeredSegments].reverse(),
|
|
)) {
|
|
const {label, text, last} = cs;
|
|
|
|
// Draw line segment
|
|
const opacityMultiplier = s.ray.isPrevious ? 0.33 : 1;
|
|
r.lineStyle(6, 0x000000, 0.5 * opacityMultiplier)
|
|
.moveTo(s.ray.A.x, s.ray.A.y)
|
|
.lineTo(s.ray.B.x, s.ray.B.y)
|
|
.lineStyle(4, rulerColor, 0.25 * opacityMultiplier)
|
|
.moveTo(s.ray.A.x, s.ray.A.y)
|
|
.lineTo(s.ray.B.x, s.ray.B.y);
|
|
|
|
// Draw the distance label just after the endpoint of the segment
|
|
if (label) {
|
|
label.text = text;
|
|
label.alpha = last ? 1.0 : 0.5;
|
|
label.visible = true;
|
|
let labelPosition = {x: s.ray.x0, y: s.ray.y0};
|
|
labelPosition.x -= label.width / 2;
|
|
labelPosition.y -= label.height / 2;
|
|
const rayLine = Line.fromPoints(s.ray.A, s.ray.B);
|
|
const rayLabelXHitY = rayLine.calcY(labelPosition.x);
|
|
let innerDistance;
|
|
// If ray hits top or bottom side of label
|
|
if (
|
|
rayLine.isVertical ||
|
|
rayLabelXHitY < labelPosition.y ||
|
|
rayLabelXHitY > labelPosition.y + label.height
|
|
)
|
|
innerDistance = Math.abs(label.height / 2 / Math.sin(s.ray.angle));
|
|
// If ray hits left or right side of label
|
|
else innerDistance = Math.abs(label.width / 2 / Math.cos(s.ray.angle));
|
|
labelPosition = s.ray.project((s.ray.distance + 50 + innerDistance) / s.ray.distance);
|
|
labelPosition.x -= label.width / 2;
|
|
labelPosition.y -= label.height / 2;
|
|
label.position.set(labelPosition.x, labelPosition.y);
|
|
}
|
|
|
|
// Highlight grid positions
|
|
if (isToken && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS && options.gridSpaces) {
|
|
if (options.enableTerrainRuler) {
|
|
highlightMeasurementTerrainRuler.call(
|
|
this,
|
|
cs.ray,
|
|
cs.startDistance,
|
|
shape,
|
|
opacityMultiplier,
|
|
);
|
|
} else {
|
|
const previousSegments = centeredSegments.slice(0, segments.length - 1 - i);
|
|
highlightMeasurementNative.call(this, cs.ray, previousSegments, shape, opacityMultiplier);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw endpoints
|
|
for (let p of waypoints) {
|
|
r.lineStyle(2, 0x000000, 0.5).beginFill(rulerColor, 0.25).drawCircle(p.x, p.y, 8);
|
|
}
|
|
|
|
// Return the measured segments
|
|
return segments;
|
|
}
|
|
|
|
export function highlightMeasurementNative(
|
|
ray,
|
|
previousSegments,
|
|
tokenShape = [{x: 0, y: 0}],
|
|
alpha = 1,
|
|
) {
|
|
const spacer = canvas.scene.data.gridType === CONST.GRID_TYPES.SQUARE ? 1.41 : 1;
|
|
const nMax = Math.max(
|
|
Math.floor(ray.distance / (spacer * Math.min(canvas.grid.w, canvas.grid.h))),
|
|
1,
|
|
);
|
|
const tMax = Array.fromRange(nMax + 1).map(t => t / nMax);
|
|
|
|
// Track prior position
|
|
let prior = null;
|
|
|
|
// Iterate over ray portions
|
|
for (let [i, t] of tMax.reverse().entries()) {
|
|
let {x, y} = ray.project(t);
|
|
|
|
// Get grid position
|
|
let [x0, y0] = i === 0 ? [null, null] : prior;
|
|
let [x1, y1] = canvas.grid.grid.getGridPositionFromPixels(x, y);
|
|
if (x0 === x1 && y0 === y1) continue;
|
|
|
|
// Highlight the grid position
|
|
let [xgtl, ygtl] = canvas.grid.grid.getPixelsFromGridPosition(x1, y1);
|
|
let [xg, yg] = canvas.grid.grid.getCenter(xgtl, ygtl);
|
|
const pathUntilSpace = previousSegments.concat([{ray: new Ray(ray.A, {x: xg, y: yg})}]);
|
|
const distance = sum(canvas.grid.measureDistances(pathUntilSpace, {gridSpaces: true}));
|
|
const color = this.dragRulerGetColorForDistance(distance);
|
|
const snapPoint = getSnapPointForToken(...canvas.grid.getTopLeft(x, y), this.draggedEntity);
|
|
const [snapX, snapY] = getGridPositionFromPixels(snapPoint.x + 1, snapPoint.y + 1);
|
|
|
|
prior = [x1, y1];
|
|
|
|
// If the positions are not neighbors, also highlight their halfway point
|
|
if (i > 0 && !canvas.grid.isNeighbor(x0, y0, x1, y1)) {
|
|
let th = tMax[i - 1] - 0.5 / nMax;
|
|
let {x, y} = ray.project(th);
|
|
let [x1h, y1h] = canvas.grid.grid.getGridPositionFromPixels(x, y);
|
|
let [xghtl, yghtl] = canvas.grid.grid.getPixelsFromGridPosition(x1h, y1h);
|
|
let [xgh, ygh] = canvas.grid.grid.getCenter(xghtl, yghtl);
|
|
const pathUntilSpace = previousSegments.concat([{ray: new Ray(ray.A, {x: xgh, y: ygh})}]);
|
|
const distance = sum(canvas.grid.measureDistances(pathUntilSpace, {gridSpaces: true}));
|
|
const color = this.dragRulerGetColorForDistance(distance);
|
|
const snapPoint = getSnapPointForToken(...canvas.grid.getTopLeft(x, y), this.draggedEntity);
|
|
const [snapX, snapY] = getGridPositionFromPixels(snapPoint.x + 1, snapPoint.y + 1);
|
|
highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color, alpha);
|
|
}
|
|
|
|
highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color, alpha);
|
|
}
|
|
}
|