Files
foundryvtt-drag-ruler/src/main.js
T

280 lines
8.7 KiB
JavaScript

"use strict";
import {
getColorForDistanceAndToken,
getMovedDistanceFromToken,
getRangesFromSpeedProvider,
initApi,
registerModule,
registerSystem,
} from "./api.js";
import {checkDependencies} from "./compatibility.js";
import {moveEntities, onMouseMove} from "./foundry_imports.js";
import {disableSnap, registerKeybindings} from "./keybindings.js";
import {libWrapper} from "./libwrapper_shim.js";
import {performMigrations} from "./migration.js";
import {removeLastHistoryEntryIfAt, resetMovementHistory} from "./movement_tracking.js";
import {extendRuler} from "./ruler.js";
import {registerSettings, RightClickAction, settingsKey} from "./settings.js";
import {recalculate} from "./socket.js";
import {SpeedProvider} from "./speed_provider.js";
import {getEntityCenter, setSnapParameterOnOptions} from "./util.js";
CONFIG.debug.dragRuler = false;
export let debugGraphics = undefined;
Hooks.once("init", () => {
registerSettings();
registerKeybindings();
initApi();
hookDragHandlers(Token);
hookDragHandlers(MeasuredTemplate);
libWrapper.register(
"drag-ruler",
"TokenLayer.prototype.undoHistory",
tokenLayerUndoHistory,
"WRAPPER",
);
extendRuler();
window.dragRuler = {
getRangesFromSpeedProvider,
getColorForDistanceAndToken,
getMovedDistanceFromToken,
registerModule,
registerSystem,
recalculate,
resetMovementHistory,
};
});
Hooks.once("ready", () => {
performMigrations();
checkDependencies();
Hooks.callAll("dragRuler.ready", SpeedProvider);
if (CONFIG.debug.dragRuler) debugGraphics = canvas.controls.addChild(new PIXI.Container());
});
Hooks.on("canvasReady", () => {
canvas.controls.rulers.children.forEach(ruler => {
ruler.draggedEntity = null;
Object.defineProperty(ruler, "isDragRuler", {
get: function isDragRuler() {
return Boolean(this.draggedEntity) && this._state !== Ruler.STATES.INACTIVE;
},
});
});
});
Hooks.on("getCombatTrackerEntryContext", function (html, menu) {
const entry = {
name: "drag-ruler.resetMovementHistory",
icon: '<i class="fas fa-undo-alt"></i>',
callback: li => resetMovementHistory(ui.combat.viewed, li.data("combatant-id")),
};
menu.splice(1, 0, entry);
});
function forwardIfUnahndled(newFn) {
return function (oldFn, ...args) {
const eventHandled = newFn(...args);
if (!eventHandled) oldFn(...args);
};
}
function hookDragHandlers(entityType) {
const entityName = entityType.name;
libWrapper.register(
"drag-ruler",
`${entityName}.prototype._onDragLeftStart`,
onEntityLeftDragStart,
"WRAPPER",
);
if (entityType === Token)
libWrapper.register(
"drag-ruler",
`${entityName}.prototype._onDragLeftMove`,
onEntityLeftDragMoveSnap,
"WRAPPER",
);
else
libWrapper.register(
"drag-ruler",
`${entityName}.prototype._onDragLeftMove`,
onEntityLeftDragMove,
"WRAPPER",
);
libWrapper.register(
"drag-ruler",
`${entityName}.prototype._onDragLeftDrop`,
forwardIfUnahndled(onEntityDragLeftDrop),
"MIXED",
);
libWrapper.register(
"drag-ruler",
`${entityName}.prototype._onDragLeftCancel`,
forwardIfUnahndled(onEntityDragLeftCancel),
"MIXED",
);
}
async function tokenLayerUndoHistory(wrapped) {
const historyEntry = this.history[this.history.length - 1];
const returnValue = await wrapped();
if (historyEntry.type === "update") {
for (const entry of historyEntry.data) {
const token = canvas.tokens.get(entry._id);
removeLastHistoryEntryIfAt(token, entry.x, entry.y);
}
}
return returnValue;
}
function onEntityLeftDragStart(wrapped, event) {
wrapped(event);
const isToken = this instanceof Token;
const ruler = canvas.controls.ruler;
ruler.draggedEntity = this;
const entityCenter = getEntityCenter(this);
ruler.rulerOffset = {
x: entityCenter.x - event.data.origin.x,
y: entityCenter.y - event.data.origin.y,
};
if (game.settings.get(settingsKey, "autoStartMeasurement")) {
let options = {};
setSnapParameterOnOptions(ruler, options);
ruler.dragRulerStart(options, false);
}
}
function onEntityLeftDragMoveSnap(wrapped, event) {
applyGridlessSnapping.call(this, event);
onEntityLeftDragMove.call(this, wrapped, event);
}
function onEntityLeftDragMove(wrapped, event) {
wrapped(event);
const ruler = canvas.controls.ruler;
if (ruler.isDragRuler) onMouseMove.call(ruler, event);
}
function onEntityDragLeftDrop(event) {
const ruler = canvas.controls.ruler;
if (!ruler.isDragRuler) {
ruler.draggedEntity = undefined;
return false;
}
// When we're dragging a measured template no token will ever be selected,
// resulting in only the dragged template to be moved as would be expected
const selectedTokens = canvas.tokens.controlled;
// This can happen if the user presses ESC during drag (maybe there are other ways too)
if (selectedTokens.length === 0) selectedTokens.push(ruler.draggedEntity);
// This can happen if the ruler is being dragged so rapidly that the drag move handler hasn't been called before dropping
if (ruler._state === Ruler.STATES.STARTING) onMouseMove.call(ruler, event);
ruler._state = Ruler.STATES.MOVING;
moveEntities.call(ruler, ruler.draggedEntity, selectedTokens);
return true;
}
function onEntityDragLeftCancel(event) {
// This function is invoked by right clicking
const ruler = canvas.controls.ruler;
if (!ruler.draggedEntity || ruler._state === Ruler.STATES.MOVING) return false;
const rightClickAction = game.settings.get(settingsKey, "rightClickAction");
let options = {};
setSnapParameterOnOptions(ruler, options);
if (ruler._state === Ruler.STATES.INACTIVE) {
if (rightClickAction !== RightClickAction.CREATE_WAYPOINT) return false;
ruler.dragRulerStart(options);
event.preventDefault();
} else if (ruler._state === Ruler.STATES.MEASURING) {
switch (rightClickAction) {
case RightClickAction.CREATE_WAYPOINT:
event.preventDefault();
ruler.dragRulerAddWaypoint(ruler.destination, options);
break;
case RightClickAction.DELETE_WAYPOINT:
ruler.dragRulerDeleteWaypoint(event, options);
break;
case RightClickAction.ABORT_DRAG:
ruler.dragRulerAbortDrag();
break;
}
}
return true;
}
function applyGridlessSnapping(event) {
const ruler = canvas.controls.ruler;
if (!game.settings.get(settingsKey, "useGridlessRaster")) return;
if (!ruler.isDragRuler) return;
if (disableSnap) return;
if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS) return;
const rasterWidth = 35 / canvas.stage.scale.x;
const tokenX = event.data.destination.x;
const tokenY = event.data.destination.y;
const destination = {x: tokenX + ruler.rulerOffset.x, y: tokenY + ruler.rulerOffset.y};
const ranges = getRangesFromSpeedProvider(ruler.draggedEntity);
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active;
if (terrainRulerAvailable) {
const segments = ruler.constructor
.dragRulerGetRaysFromWaypoints(ruler.waypoints, destination)
.map(ray => {
return {ray};
});
const pinpointDistances = new Map();
for (const range of ranges) {
pinpointDistances.set(range.range, null);
}
terrainRuler.measureDistances(segments, {pinpointDistances});
const targetDistance = Array.from(pinpointDistances.entries())
.filter(([_key, val]) => val)
.reduce((value, current) => (value[0] > current[0] ? value : current), [0, null]);
const rasterLocation = targetDistance[1];
if (rasterLocation) {
const deltaX = destination.x - rasterLocation.x;
const deltaY = destination.y - rasterLocation.y;
const rasterDistance = Math.hypot(deltaX, deltaY);
if (rasterDistance < rasterWidth) {
event.data.destination.x = rasterLocation.x - ruler.rulerOffset.x;
event.data.destination.y = rasterLocation.y - ruler.rulerOffset.y;
}
}
} else {
let waypointDistance = 0;
let origin = event.data.origin;
if (ruler.waypoints.length > 1) {
const segments = ruler.constructor
.dragRulerGetRaysFromWaypoints(ruler.waypoints, destination)
.map(ray => {
return {ray};
});
origin = segments.pop().ray.A;
waypointDistance = canvas.grid.measureDistances(segments).reduce((a, b) => a + b);
origin = {x: origin.x - ruler.rulerOffset.x, y: origin.y - ruler.rulerOffset.y};
}
const deltaX = tokenX - origin.x;
const deltaY = tokenY - origin.y;
const distance = Math.hypot(deltaX, deltaY);
// targetRange will be the largest range that's still smaller than distance
let targetDistance = ranges
.map(range => range.range)
.map(range => range - waypointDistance)
.map(range => (range * canvas.dimensions.size) / canvas.dimensions.distance)
.filter(range => range < distance)
.reduce((a, b) => Math.max(a, b), 0);
if (targetDistance) {
if (distance < targetDistance + rasterWidth) {
event.data.destination.x = origin.x + (deltaX * targetDistance) / distance;
event.data.destination.y = origin.y + (deltaY * targetDistance) / distance;
}
}
}
}