Rename js/ to src/ since the rust component is now gone
This commit is contained in:
+206
@@ -0,0 +1,206 @@
|
||||
import {measureDistances} from "./compatibility.js";
|
||||
import {getMovementHistory} from "./movement_tracking.js";
|
||||
import {GenericSpeedProvider, SpeedProvider} from "./speed_provider.js";
|
||||
import {settingsKey} from "./settings.js";
|
||||
import {getAreaFromPositionAndShape, getTokenShape} from "./util.js";
|
||||
|
||||
export const availableSpeedProviders = {};
|
||||
export let currentSpeedProvider = undefined;
|
||||
|
||||
function register(module, type, speedProvider) {
|
||||
const id = `${type}.${module.id}`;
|
||||
let providerInstance;
|
||||
if (speedProvider.prototype instanceof SpeedProvider) {
|
||||
providerInstance = new speedProvider(id);
|
||||
} else {
|
||||
console.warn(
|
||||
`Drag Ruler | The ${type} '${module.id}' uses the old, deprecated version of the Drag Ruler API. ` +
|
||||
"That old API will be removed in a future Drag Ruler version. " +
|
||||
`Please update the ${type} ${module.id} to stay compatible with future Drag Ruler versions.`,
|
||||
);
|
||||
speedProvider.id = id;
|
||||
speedProvider.usesRuler = () => true;
|
||||
providerInstance = speedProvider;
|
||||
}
|
||||
setupProvider(providerInstance);
|
||||
}
|
||||
|
||||
function setupProvider(speedProvider) {
|
||||
if (speedProvider instanceof SpeedProvider) {
|
||||
const unreachableColor = {
|
||||
id: "unreachable",
|
||||
default: speedProvider.defaultUnreachableColor,
|
||||
name: "drag-ruler.settings.speedProviderSettings.color.unreachable.name",
|
||||
};
|
||||
for (const color of speedProvider.colors.concat([unreachableColor])) {
|
||||
game.settings.register(settingsKey, `speedProviders.${speedProvider.id}.color.${color.id}`, {
|
||||
config: false,
|
||||
scope: "client",
|
||||
type: Number,
|
||||
default: color.default,
|
||||
});
|
||||
}
|
||||
for (const setting of speedProvider.settings) {
|
||||
setting.config = false;
|
||||
game.settings.register(
|
||||
settingsKey,
|
||||
`speedProviders.${speedProvider.id}.setting.${setting.id}`,
|
||||
setting,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
availableSpeedProviders[speedProvider.id] = speedProvider;
|
||||
game.settings.settings.get("drag-ruler.speedProvider").default = getDefaultSpeedProvider();
|
||||
updateSpeedProvider();
|
||||
}
|
||||
|
||||
export function getDefaultSpeedProvider() {
|
||||
const providerIds = Object.keys(availableSpeedProviders);
|
||||
// Game systems take the highest precedence for the being the default
|
||||
const gameSystem = providerIds.find(key => key.startsWith("system."));
|
||||
if (gameSystem) return gameSystem;
|
||||
|
||||
// If no game system is registered modules are next up.
|
||||
// For lack of a method to select the best module we're just falling back to taking the next best module
|
||||
// settingKeys should always be sorted the same way so this should achive a stable default
|
||||
const module = providerIds.find(key => key.startsWith("module."));
|
||||
if (module) return module;
|
||||
|
||||
// If neither a game system or a module is found fall back to the native implementation
|
||||
return providerIds[0];
|
||||
}
|
||||
|
||||
export function updateSpeedProvider() {
|
||||
// If the configured provider is registered use that one. If not use the default provider
|
||||
const configuredProvider = game.settings.get("drag-ruler", "speedProvider");
|
||||
currentSpeedProvider =
|
||||
availableSpeedProviders[configuredProvider] ??
|
||||
availableSpeedProviders[game.settings.settings.get("drag-ruler.speedProvider").default];
|
||||
}
|
||||
|
||||
export function initApi() {
|
||||
const genericSpeedProviderInstance = new GenericSpeedProvider("native");
|
||||
setupProvider(genericSpeedProviderInstance);
|
||||
}
|
||||
|
||||
export function getRangesFromSpeedProvider(token) {
|
||||
try {
|
||||
if (currentSpeedProvider instanceof Function) return currentSpeedProvider(token, 0x00ff00);
|
||||
const ranges = currentSpeedProvider.getRanges(token);
|
||||
for (const range of ranges) {
|
||||
range.color = game.settings.get(
|
||||
settingsKey,
|
||||
`speedProviders.${currentSpeedProvider.id}.color.${range.color}`,
|
||||
);
|
||||
}
|
||||
return ranges;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function getUnreachableColorFromSpeedProvider() {
|
||||
if (currentSpeedProvider instanceof Function) return 0xff0000;
|
||||
try {
|
||||
return game.settings.get(
|
||||
settingsKey,
|
||||
`speedProviders.${currentSpeedProvider.id}.color.unreachable`,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return 0xff0000;
|
||||
}
|
||||
}
|
||||
|
||||
export function getCostFromSpeedProvider(token, area, options) {
|
||||
try {
|
||||
if (currentSpeedProvider instanceof Function) {
|
||||
return SpeedProvider.prototype.getCostForStep.call(undefined, token, area, options);
|
||||
}
|
||||
return currentSpeedProvider.getCostForStep(token, area, options);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
export function getColorForDistanceAndToken(distance, token, ranges = null) {
|
||||
if (!ranges) {
|
||||
ranges = getRangesFromSpeedProvider(token);
|
||||
}
|
||||
if (ranges.length === 0) return null;
|
||||
const currentRange = ranges.reduce(
|
||||
(minRange, currentRange) => {
|
||||
if (distance <= currentRange.range && currentRange.range < minRange.range)
|
||||
return currentRange;
|
||||
return minRange;
|
||||
},
|
||||
{range: Infinity, color: getUnreachableColorFromSpeedProvider()},
|
||||
);
|
||||
return currentRange.color;
|
||||
}
|
||||
|
||||
export function getMovedDistanceFromToken(token) {
|
||||
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active;
|
||||
const history = getMovementHistory(token);
|
||||
const segments = CONFIG.Canvas.rulerClass
|
||||
.dragRulerGetRaysFromWaypoints(history, {x: token.x, y: token.y})
|
||||
.map(ray => {
|
||||
return {ray};
|
||||
});
|
||||
const shape = getTokenShape(token);
|
||||
const distances = measureDistances(segments, token, shape, {
|
||||
enableTerrainRuler: terrainRulerAvailable,
|
||||
});
|
||||
// Sum up the distances
|
||||
return distances.reduce((acc, val) => acc + val, 0);
|
||||
}
|
||||
|
||||
export function buildCostFunction(token, shape) {
|
||||
return (x, y, costOptions = {}) =>
|
||||
getCostFromSpeedProvider(token, getAreaFromPositionAndShape({x, y}, shape), costOptions);
|
||||
}
|
||||
|
||||
export function registerModule(moduleId, speedProvider) {
|
||||
// Check if a module with the given id exists and is currently enabled
|
||||
const module = game.modules.get(moduleId);
|
||||
// If it doesn't the calling module did something wrong. Log a warning and ignore this module
|
||||
if (!module) {
|
||||
console.warn(
|
||||
`Drag Ruler | A module tried to register with the id "${moduleId}". However no active module with this id was found.` +
|
||||
"This api registration call was ignored. " +
|
||||
"If you are the author of that module please check that the id passed to `registerModule` matches the id in your manifest exactly." +
|
||||
"If this call was made form a game system instead of a module please use `registerSystem` instead.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Using Drag Ruler's id is not allowed
|
||||
if (moduleId === "drag-ruler") {
|
||||
console.warn(
|
||||
`Drag Ruler | A module tried to register with the id "${moduleId}", which is not allowed. This api registration call was ignored. ` +
|
||||
"If you're the author of the module please use the id of your own module as it's specified in your manifest to register to this api. " +
|
||||
"If this call was made form a game system instead of a module please use `registerSystem` instead.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
register(module, "module", speedProvider);
|
||||
}
|
||||
|
||||
export function registerSystem(systemId, speedProvider) {
|
||||
const system = game.system;
|
||||
// If the current system id doesn't match the provided id something went wrong. Log a warning and ignore this module
|
||||
if (system.id != systemId) {
|
||||
console.warn(
|
||||
`Drag Ruler | A system tried to register with the id "${systemId}". However the active system has a different id.` +
|
||||
"This api registration call was ignored. " +
|
||||
"If you are the author of that system please check that the id passed to `registerSystem` matches the id in your manifest exactly." +
|
||||
"If this call was made form a module instead of a game system please use `registerModule` instead.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
register(system, "system", speedProvider);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import {buildCostFunction} from "./api.js";
|
||||
import {settingsKey} from "./settings.js";
|
||||
import {highlightTokenShape} from "./util.js";
|
||||
|
||||
export function getHexSizeSupportTokenGridCenter(token) {
|
||||
const tokenCenterOffset = CONFIG.hexSizeSupport.getCenterOffset(token);
|
||||
return {x: token.x + tokenCenterOffset.x, y: token.y + tokenCenterOffset.y};
|
||||
}
|
||||
|
||||
export function highlightMeasurementTerrainRuler(
|
||||
ray,
|
||||
startDistance,
|
||||
tokenShape = [{x: 0, y: 0}],
|
||||
alpha = 1,
|
||||
) {
|
||||
for (const space of ray.terrainRulerVisitedSpaces.reverse()) {
|
||||
const color = this.dragRulerGetColorForDistance(startDistance + space.distance);
|
||||
highlightTokenShape.call(this, space, tokenShape, color, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
export function measureDistances(segments, entity, shape, options = {}) {
|
||||
const opts = duplicate(options);
|
||||
if (canvas.grid.diagonalRule === "EUCL") {
|
||||
opts.ignoreGrid = true;
|
||||
opts.gridSpaes = false;
|
||||
}
|
||||
if (opts.enableTerrainRuler) {
|
||||
opts.gridSpaces = true;
|
||||
const firstNewSegmentIndex = segments.findIndex(segment => !segment.ray.dragRulerVisitedSpaces);
|
||||
const previousSegments = segments.slice(0, firstNewSegmentIndex);
|
||||
const newSegments = segments.slice(firstNewSegmentIndex);
|
||||
const distances = previousSegments.map(
|
||||
segment =>
|
||||
segment.ray.dragRulerVisitedSpaces[segment.ray.dragRulerVisitedSpaces.length - 1].distance,
|
||||
);
|
||||
previousSegments.forEach(
|
||||
segment =>
|
||||
(segment.ray.terrainRulerVisitedSpaces = duplicate(segment.ray.dragRulerVisitedSpaces)),
|
||||
);
|
||||
opts.costFunction = buildCostFunction(entity, shape);
|
||||
if (previousSegments.length > 0)
|
||||
opts.terrainRulerInitialState =
|
||||
previousSegments[previousSegments.length - 1].ray.dragRulerFinalState;
|
||||
return distances.concat(terrainRuler.measureDistances(newSegments, opts));
|
||||
} else {
|
||||
// If another module wants to enable grid measurements but disable grid highlighting,
|
||||
// manually set the *duplicate* option's gridSpaces value to true for the Foundry logic to work properly
|
||||
if (!opts.ignoreGrid) {
|
||||
opts.gridSpaces = true;
|
||||
}
|
||||
return canvas.grid.measureDistances(segments, opts);
|
||||
}
|
||||
}
|
||||
|
||||
export function checkDependencies() {
|
||||
if (!game.modules.get("socketlib")?.active) {
|
||||
console.error(
|
||||
"Drag Ruler | The `socketlib` module isn't enabled, but it's required for Drag Ruler to operate properly.",
|
||||
);
|
||||
if (game.user.isGM) {
|
||||
new Dialog({
|
||||
title: game.i18n.localize("drag-ruler.dependencies.socketlib.title"),
|
||||
content: `<h2>${game.i18n.localize(
|
||||
"drag-ruler.dependencies.socketlib.title",
|
||||
)}</h2><p>${game.i18n.localize("drag-ruler.dependencies.socketlib.text")}</p>`,
|
||||
buttons: {
|
||||
ok: {
|
||||
icon: '<i class="fas fa-check"></i>',
|
||||
label: game.i18n.localize("drag-ruler.dependencies.ok"),
|
||||
},
|
||||
},
|
||||
}).render(true);
|
||||
}
|
||||
} else if (
|
||||
!game.modules.get("terrain-ruler")?.active &&
|
||||
game.user.isGM &&
|
||||
!game.settings.get(settingsKey, "neverShowTerrainRulerHint")
|
||||
) {
|
||||
const lastHint = game.settings.get(settingsKey, "lastTerrainRulerHintTime");
|
||||
if (Date.now() - lastHint > 604800000) {
|
||||
// One week
|
||||
let enabledTerrainModule;
|
||||
if (game.modules.get("enhanced-terrain-layer")?.active) {
|
||||
enabledTerrainModule = game.modules.get("enhanced-terrain-layer").data.title;
|
||||
}
|
||||
if (enabledTerrainModule) {
|
||||
new Dialog({
|
||||
title: game.i18n.localize("drag-ruler.dependencies.terrain-ruler.title"),
|
||||
content: `<h2>${game.i18n.localize(
|
||||
"drag-ruler.dependencies.terrain-ruler.title",
|
||||
)}</h2><p>${game.i18n.format("drag-ruler.dependencies.terrain-ruler.text", {
|
||||
moduleName: enabledTerrainModule,
|
||||
})}</p>`,
|
||||
buttons: {
|
||||
ok: {
|
||||
icon: '<i class="fas fa-check"></i>',
|
||||
label: game.i18n.localize("drag-ruler.dependencies.ok"),
|
||||
callback: () =>
|
||||
game.settings.set(settingsKey, "lastTerrainRulerHintTime", Date.now()),
|
||||
},
|
||||
neverShowAgain: {
|
||||
icon: '<i class="fas fa-times"></i>',
|
||||
label: game.i18n.localize("drag-ruler.dependencies.terrain-ruler.neverShowAgain"),
|
||||
callback: () => game.settings.set(settingsKey, "neverShowTerrainRulerHint", true),
|
||||
},
|
||||
},
|
||||
close: () => game.settings.set(settingsKey, "lastTerrainRulerHintTime", Date.now()),
|
||||
}).render(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* A combination queue/set where the elements are ordered (in ascending order, according to the given priority function)
|
||||
* and unique (according to the given elementMatcher).
|
||||
*
|
||||
* If an element is added to the set and an equivalent element already exists, the lower-priority one is discarded.
|
||||
*/
|
||||
export class PriorityQueueSet {
|
||||
constructor(elementMatcher, priorityFunction) {
|
||||
this.first = null;
|
||||
this.elementMatcher = elementMatcher;
|
||||
this.priorityFunction = priorityFunction;
|
||||
}
|
||||
|
||||
pushWithPriority(value) {
|
||||
const newNode = {value, priority: this.priorityFunction(value), next: null};
|
||||
|
||||
// If the queue is currently empty, we can just set this new node as the first and we're done
|
||||
if (!this.first) {
|
||||
this.first = newNode;
|
||||
return;
|
||||
}
|
||||
|
||||
let inserted = false;
|
||||
let previous;
|
||||
let current = this.first;
|
||||
|
||||
// Loop through the existing elements
|
||||
while (current) {
|
||||
if (this.elementMatcher(current.value, value)) {
|
||||
// We've found an equivalent element before one with a lower priority. This one has at least
|
||||
// the same priority as the new one, so don't bother inserting
|
||||
return;
|
||||
} else if (newNode.priority <= current.priority) {
|
||||
// We've found some element with lower priority than the new one, so insert the new one just before it
|
||||
newNode.next = current;
|
||||
if (previous) {
|
||||
previous.next = newNode;
|
||||
} else {
|
||||
this.first = newNode;
|
||||
}
|
||||
inserted = true;
|
||||
|
||||
previous = current;
|
||||
current = current.next;
|
||||
break;
|
||||
}
|
||||
previous = current;
|
||||
current = current.next;
|
||||
}
|
||||
|
||||
if (inserted) {
|
||||
// Go through the rest of the list and try to find an equivalent element to the new one.
|
||||
// We know it has higher priority than the new one, so remove it.
|
||||
while (current) {
|
||||
if (this.elementMatcher(current.value, value)) {
|
||||
if (previous) {
|
||||
previous.next = current.next;
|
||||
} else {
|
||||
this.first = current.next;
|
||||
}
|
||||
return;
|
||||
}
|
||||
previous = current;
|
||||
current = current.next;
|
||||
}
|
||||
} else {
|
||||
// We reached the end of the queue without finding a lower-priority or existing element, so
|
||||
// insert the new one at the end
|
||||
previous.next = newNode;
|
||||
}
|
||||
}
|
||||
|
||||
hasNext() {
|
||||
return !!this.first;
|
||||
}
|
||||
|
||||
pop() {
|
||||
const first = this.first;
|
||||
this.first = first?.next;
|
||||
return first?.value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue that will only ever accept elements with a given value once. Elements must have a "value" field, the
|
||||
* JSON representation of which will be used as the key to match
|
||||
*/
|
||||
export class ProcessOnceQueue {
|
||||
constructor() {
|
||||
this.first = null;
|
||||
this.last = null;
|
||||
this.previouslyQueued = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove everything from the queue and forget all the previously-queued items
|
||||
*/
|
||||
reset() {
|
||||
this.first = null;
|
||||
this.last = null;
|
||||
this.previouslyQueued.clear();
|
||||
}
|
||||
|
||||
push(element) {
|
||||
if (this.previouslyQueued.has(element)) {
|
||||
return;
|
||||
}
|
||||
this.previouslyQueued.add(element);
|
||||
|
||||
const newNode = {
|
||||
value: element,
|
||||
next: null,
|
||||
previous: null,
|
||||
};
|
||||
|
||||
if (!this.first) {
|
||||
this.first = newNode;
|
||||
this.last = newNode;
|
||||
} else {
|
||||
this.last.next = newNode;
|
||||
newNode.previous = this.last;
|
||||
this.last = newNode;
|
||||
}
|
||||
}
|
||||
|
||||
pop() {
|
||||
const node = this.first;
|
||||
this.first = node?.next;
|
||||
if (!node?.next) {
|
||||
this.last = null;
|
||||
}
|
||||
return node?.value;
|
||||
}
|
||||
|
||||
hasNext() {
|
||||
return !!this.first;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Wrapper to fix a FoundryVTT bug that causes the return values of canvas.grid.grid.getPixelsFromGridPosition to be ordered inconsistently
|
||||
|
||||
// https://gitlab.com/foundrynet/foundryvtt/-/issues/4705
|
||||
export function getPixelsFromGridPosition(xGrid, yGrid) {
|
||||
if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS) {
|
||||
return canvas.grid.grid.getPixelsFromGridPosition(yGrid, xGrid);
|
||||
}
|
||||
return canvas.grid.grid.getPixelsFromGridPosition(xGrid, yGrid);
|
||||
}
|
||||
|
||||
// Wrapper to fix a FoundryVTT bug that causes the return values of canvas.grid.grid.getPixelsFromGridPosition to be ordered inconsistently
|
||||
// https://gitlab.com/foundrynet/foundryvtt/-/issues/4705
|
||||
export function getGridPositionFromPixels(xPixel, yPixel) {
|
||||
const [x, y] = canvas.grid.grid.getGridPositionFromPixels(xPixel, yPixel);
|
||||
if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS) return [y, x];
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
export function getGridPositionFromPixelsObj(o) {
|
||||
const r = {};
|
||||
[r.x, r.y] = getGridPositionFromPixels(o.x, o.y);
|
||||
return r;
|
||||
}
|
||||
|
||||
export function getPixelsFromGridPositionObj(o) {
|
||||
const r = {};
|
||||
[r.x, r.y] = getPixelsFromGridPosition(o.x, o.y);
|
||||
return r;
|
||||
}
|
||||
|
||||
export function getCenterFromGridPositionObj(o) {
|
||||
const r = getPixelsFromGridPositionObj(o);
|
||||
[r.x, r.y] = canvas.grid.getCenter(r.x, r.y);
|
||||
return r;
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
import {getGridPositionFromPixels} from "./foundry_fixes.js";
|
||||
import {disableSnap, moveWithoutAnimation} from "./keybindings.js";
|
||||
import {trackRays} from "./movement_tracking.js";
|
||||
import {recalculate} from "./socket.js";
|
||||
import {getSnapPointForToken, 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.x - origin[0];
|
||||
dy = entity.y - origin[1];
|
||||
} else {
|
||||
dx = entity.x - origin[0];
|
||||
dy = entity.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.x - entityB.x, y: entityA.y - entityB.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?.();
|
||||
}
|
||||
|
||||
export function highlightMeasurementNative(
|
||||
ray,
|
||||
previousSegments,
|
||||
tokenShape = [{x: 0, y: 0}],
|
||||
alpha = 1,
|
||||
) {
|
||||
const spacer = canvas.scene.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export class Line {
|
||||
constructor(m, b) {
|
||||
this.m = m;
|
||||
this.b = b;
|
||||
}
|
||||
|
||||
static fromPoints(p1, p2) {
|
||||
// Bring line into y=mx+b form
|
||||
const m = (p1.y - p2.y) / (p1.x - p2.x);
|
||||
const b = p1.y - m * p1.x;
|
||||
return new Line(m, b);
|
||||
}
|
||||
|
||||
get isVertical() {
|
||||
return !isFinite(this.m);
|
||||
}
|
||||
|
||||
calcY(x) {
|
||||
return this.m * x + this.b;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Functions taken directly from the hex-size-support module (https://github.com/Ourobor/Hex-Size-Support/releases/tag/1.1.0).
|
||||
* Unless otherwise stated, these functions are taken as-is.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Altered version of this function.
|
||||
* - Instead of taking a token as a parameter to retrieve the altOrientationFlag, receive the flag value directly
|
||||
* - Instead of taking a grid parameter, get the grid value from the globas canvas
|
||||
*/
|
||||
export function findVertexSnapPoint(x, y, altOrientationFlag) {
|
||||
const grid = canvas.grid.grid;
|
||||
if (grid.columns) {
|
||||
return findSnapPointCols(x, y, grid.h, grid.w, altOrientationFlag);
|
||||
} else {
|
||||
return findSnapPointRows(x, y, grid.h, grid.w, altOrientationFlag);
|
||||
}
|
||||
}
|
||||
|
||||
function findSnapPointRows(x, y, h, w, alt) {
|
||||
let xOffset = 0.0;
|
||||
if (canvas.grid.grid.even) {
|
||||
xOffset = -0.5;
|
||||
}
|
||||
|
||||
let yOffset1 = 0.75;
|
||||
let yOffset2 = 0.0;
|
||||
if (alt) {
|
||||
yOffset1 = 0.25;
|
||||
yOffset2 = 1.0;
|
||||
}
|
||||
|
||||
let row1 = calculateSnapPointsRows(x, y, h, w, 0.5 + xOffset, yOffset1);
|
||||
let row2 = calculateSnapPointsRows(x, y, h, w, 1.0 + xOffset, yOffset2);
|
||||
|
||||
let dist1 = Math.pow(row1.x - x, 2) + Math.pow(row1.y - y, 2);
|
||||
let dist2 = Math.pow(row2.x - x, 2) + Math.pow(row2.y - y, 2);
|
||||
|
||||
if (dist1 < dist2) {
|
||||
return row1;
|
||||
} else {
|
||||
return row2;
|
||||
}
|
||||
}
|
||||
|
||||
function calculateSnapPointsRows(x, y, h, w, xOff, yOff) {
|
||||
let c = Math.floor((x + (0.5 - xOff) * w) / w + 1);
|
||||
let r = Math.floor((y + (0.75 - yOff) * h) / (1.5 * h) + 1);
|
||||
|
||||
let snapX = c * w - (1 - xOff) * w;
|
||||
let snapY = r * h * 1.5 - (1.5 - yOff) * h;
|
||||
|
||||
return {x: snapX, y: snapY};
|
||||
}
|
||||
|
||||
function findSnapPointCols(x, y, h, w, alt) {
|
||||
let yOffset = 0.0;
|
||||
if (canvas.grid.grid.even) {
|
||||
yOffset = -0.5;
|
||||
}
|
||||
|
||||
let xOffset1 = 0.25;
|
||||
let xOffset2 = 1.0;
|
||||
if (alt) {
|
||||
xOffset1 = 0.75;
|
||||
xOffset2 = 0.0;
|
||||
}
|
||||
|
||||
let row1 = calculateSnapPointsCols(x, y, h, w, xOffset1, 0.5 + yOffset);
|
||||
let row2 = calculateSnapPointsCols(x, y, h, w, xOffset2, 1.0 + yOffset);
|
||||
|
||||
let dist1 = Math.pow(row1.x - x, 2) + Math.pow(row1.y - y, 2);
|
||||
let dist2 = Math.pow(row2.x - x, 2) + Math.pow(row2.y - y, 2);
|
||||
|
||||
if (dist1 < dist2) {
|
||||
return row1;
|
||||
} else {
|
||||
return row2;
|
||||
}
|
||||
}
|
||||
|
||||
function calculateSnapPointsCols(x, y, h, w, xOff, yOff) {
|
||||
let c = Math.floor((x + (0.75 - xOff) * w) / (1.5 * w) + 1);
|
||||
let r = Math.floor((y + (0.5 - yOff) * h) / h + 1);
|
||||
|
||||
let snapX = c * w * 1.5 - (1.5 - xOff) * w;
|
||||
let snapY = r * h - (1 - yOff) * h;
|
||||
|
||||
return {x: snapX, y: snapY};
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import {settingsKey} from "./settings.js";
|
||||
import {getMeasurePosition, setSnapParameterOnOptions} from "./util.js";
|
||||
|
||||
// Indicates whether the user is currently pressing the button to disable snapping
|
||||
export let disableSnap = false;
|
||||
|
||||
// Indicates whether the user is currently pressing the button to disable the movement animation
|
||||
export let moveWithoutAnimation = false;
|
||||
|
||||
// Indicates whether the user is pressing the pathfinding toggle button
|
||||
export let togglePathfinding = false;
|
||||
|
||||
export function registerKeybindings() {
|
||||
game.keybindings.register(settingsKey, "cancelDrag", {
|
||||
name: "drag-ruler.keybindings.cancelDrag",
|
||||
onDown: cancelDrag,
|
||||
uneditable: [
|
||||
{
|
||||
key: "Escape",
|
||||
},
|
||||
],
|
||||
precedence: -1,
|
||||
});
|
||||
|
||||
game.keybindings.register(settingsKey, "createWaypoint", {
|
||||
name: "drag-ruler.keybindings.createWaypoint",
|
||||
onDown: handleCreateWaypoint,
|
||||
editable: [
|
||||
{
|
||||
key: "Space",
|
||||
},
|
||||
],
|
||||
precedence: -1,
|
||||
});
|
||||
|
||||
game.keybindings.register(settingsKey, "deleteWaypoint", {
|
||||
name: "drag-ruler.keybindings.deleteWaypoint",
|
||||
onDown: handleDeleteWaypoint,
|
||||
precedence: -1,
|
||||
});
|
||||
|
||||
game.keybindings.register(settingsKey, "disableSnap", {
|
||||
name: "drag-ruler.keybindings.disableSnap.name",
|
||||
hint: "drag-ruler.keybindings.disableSnap.hint",
|
||||
onDown: handleDisableSnap,
|
||||
onUp: handleDisableSnap,
|
||||
editable: [
|
||||
{
|
||||
key: "ShiftLeft",
|
||||
},
|
||||
],
|
||||
precedence: -1,
|
||||
});
|
||||
|
||||
game.keybindings.register(settingsKey, "moveWithoutAnimation", {
|
||||
name: "drag-ruler.keybindings.moveWithoutAnimation.name",
|
||||
hint: "drag-ruler.keybindings.moveWithoutAnimation.hint",
|
||||
onDown: handleMoveWithoutAnimation,
|
||||
onUp: handleMoveWithoutAnimation,
|
||||
editable: [
|
||||
{
|
||||
key: "AltLeft",
|
||||
},
|
||||
],
|
||||
precedence: -1,
|
||||
});
|
||||
|
||||
if (game.modules.get("routinglib")?.active) {
|
||||
game.keybindings.register(settingsKey, "togglePathfinding", {
|
||||
name: "drag-ruler.keybindings.togglePathfinding.name",
|
||||
hint: "drag-ruler.keybindings.togglePathfinding.hint",
|
||||
onDown: handleTogglePathfinding,
|
||||
onUp: handleTogglePathfinding,
|
||||
precedence: -1,
|
||||
restricted: !game.settings.get(settingsKey, "allowPathfinding"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteWaypoint() {
|
||||
const ruler = canvas.controls.ruler;
|
||||
if (!ruler?.draggedEntity) return false;
|
||||
ruler.dragRulerDeleteWaypoint();
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleCreateWaypoint() {
|
||||
const ruler = canvas.controls.ruler;
|
||||
// .draggedEntity is used here because .isDragRuler only returns true once the ruler started measuring
|
||||
// Ruler can end up being undefined here if no canvas is active
|
||||
if (!ruler?.draggedEntity) return false;
|
||||
|
||||
let options = {};
|
||||
setSnapParameterOnOptions(ruler, options);
|
||||
|
||||
if (ruler._state === Ruler.STATES.INACTIVE) {
|
||||
ruler.dragRulerStart(options);
|
||||
} else {
|
||||
ruler.dragRulerAddWaypoint(getMeasurePosition(), options);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function cancelDrag() {
|
||||
const ruler = canvas.controls.ruler;
|
||||
if (!ruler?.draggedEntity) return false;
|
||||
ruler.dragRulerAbortDrag();
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleDisableSnap(event) {
|
||||
disableSnap = !event.up;
|
||||
|
||||
const ruler = canvas.controls.ruler;
|
||||
if (!ruler?.isDragRuler) return false;
|
||||
if (ruler._state !== Ruler.STATES.MEASURING) return false;
|
||||
|
||||
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
|
||||
ruler.dragRulerSendState();
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleMoveWithoutAnimation(event) {
|
||||
moveWithoutAnimation = !event.up;
|
||||
|
||||
const ruler = canvas.controls.ruler;
|
||||
if (!ruler?.isDragRuler) return false;
|
||||
if (ruler._state !== Ruler.STATES.MEASURING) return false;
|
||||
|
||||
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
|
||||
ruler.dragRulerSendState();
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleTogglePathfinding(event) {
|
||||
togglePathfinding = !event.up;
|
||||
|
||||
const ruler = canvas.controls.ruler;
|
||||
if (!ruler?.isDragRuler) return false;
|
||||
if (ruler._state !== Ruler.STATES.MEASURING) return false;
|
||||
|
||||
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
|
||||
ruler.dragRulerSendState();
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2021 fvtt-lib-wrapper Rui Pinheiro
|
||||
|
||||
"use strict";
|
||||
|
||||
// A shim for the libWrapper library
|
||||
export let libWrapper = undefined;
|
||||
|
||||
export const VERSIONS = [1, 11, 0];
|
||||
export const TGT_SPLIT_RE = new RegExp(
|
||||
"([^.[]+|\\[('([^'\\\\]|\\\\.)+?'|\"([^\"\\\\]|\\\\.)+?\")\\])",
|
||||
"g",
|
||||
);
|
||||
export const TGT_CLEANUP_RE = new RegExp("(^\\['|'\\]$|^\\[\"|\"\\]$)", "g");
|
||||
|
||||
// Main shim code
|
||||
Hooks.once("init", () => {
|
||||
// Check if the real module is already loaded - if so, use it
|
||||
if (globalThis.libWrapper && !(globalThis.libWrapper.is_fallback ?? true)) {
|
||||
libWrapper = globalThis.libWrapper;
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback implementation
|
||||
libWrapper = class {
|
||||
static get is_fallback() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static get WRAPPER() {
|
||||
return "WRAPPER";
|
||||
}
|
||||
static get MIXED() {
|
||||
return "MIXED";
|
||||
}
|
||||
static get OVERRIDE() {
|
||||
return "OVERRIDE";
|
||||
}
|
||||
|
||||
static register(package_id, target, fn, type = "MIXED", {chain = undefined} = {}) {
|
||||
const is_setter = target.endsWith("#set");
|
||||
target = !is_setter ? target : target.slice(0, -4);
|
||||
const split = target
|
||||
.match(TGT_SPLIT_RE)
|
||||
.map(x => x.replace(/\\(.)/g, "$1").replace(TGT_CLEANUP_RE, ""));
|
||||
const root_nm = split.splice(0, 1)[0];
|
||||
|
||||
let obj, fn_name;
|
||||
if (split.length == 0) {
|
||||
obj = globalThis;
|
||||
fn_name = root_nm;
|
||||
} else {
|
||||
const _eval = eval;
|
||||
fn_name = split.pop();
|
||||
obj = split.reduce((x, y) => x[y], globalThis[root_nm] ?? _eval(root_nm));
|
||||
}
|
||||
|
||||
let iObj = obj;
|
||||
let descriptor = null;
|
||||
while (iObj) {
|
||||
descriptor = Object.getOwnPropertyDescriptor(iObj, fn_name);
|
||||
if (descriptor) break;
|
||||
iObj = Object.getPrototypeOf(iObj);
|
||||
}
|
||||
if (!descriptor || descriptor?.configurable === false)
|
||||
throw `libWrapper Shim: '${target}' does not exist, could not be found, or has a non-configurable descriptor.`;
|
||||
|
||||
let original = null;
|
||||
const wrapper =
|
||||
chain ?? (type.toUpperCase?.() != "OVERRIDE" && type != 3)
|
||||
? function () {
|
||||
return fn.call(this, original.bind(this), ...arguments);
|
||||
}
|
||||
: function () {
|
||||
return fn.apply(this, arguments);
|
||||
};
|
||||
|
||||
if (!is_setter) {
|
||||
if (descriptor.value) {
|
||||
original = descriptor.value;
|
||||
descriptor.value = wrapper;
|
||||
} else {
|
||||
original = descriptor.get;
|
||||
descriptor.get = wrapper;
|
||||
}
|
||||
} else {
|
||||
if (!descriptor.set) throw `libWrapper Shim: '${target}' does not have a setter`;
|
||||
original = descriptor.set;
|
||||
descriptor.set = wrapper;
|
||||
}
|
||||
|
||||
descriptor.configurable = true;
|
||||
Object.defineProperty(obj, fn_name, descriptor);
|
||||
}
|
||||
};
|
||||
});
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
"use strict";
|
||||
|
||||
import {
|
||||
getColorForDistanceAndToken,
|
||||
getMovedDistanceFromToken,
|
||||
getRangesFromSpeedProvider,
|
||||
initApi,
|
||||
registerModule,
|
||||
registerSystem,
|
||||
} from "./api.js";
|
||||
import {checkDependencies, getHexSizeSupportTokenGridCenter} 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 {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 = {
|
||||
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;
|
||||
let entityCenter;
|
||||
if (
|
||||
isToken &&
|
||||
canvas.grid.isHex &&
|
||||
game.modules.get("hex-size-support")?.active &&
|
||||
CONFIG.hexSizeSupport.getAltSnappingFlag(this)
|
||||
)
|
||||
entityCenter = getHexSizeSupportTokenGridCenter(this);
|
||||
else entityCenter = this.center;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import {RightClickAction, settingsKey} from "./settings.js";
|
||||
|
||||
const currentDataVersion = "1.10.0";
|
||||
|
||||
export async function performMigrations() {
|
||||
if (game.user.isGM) await performWorldMigraionts();
|
||||
await performClientMigrations();
|
||||
}
|
||||
|
||||
async function performWorldMigraionts() {
|
||||
let dataVersion = game.settings.get(settingsKey, "dataVersion");
|
||||
|
||||
if (dataVersion === currentDataVersion) return;
|
||||
|
||||
if (dataVersion === "fresh install") {
|
||||
game.settings.set(settingsKey, "dataVersion", currentDataVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataVersion === "1.3.0") {
|
||||
dataVersion = "1.10.0";
|
||||
}
|
||||
|
||||
game.settings.set(settingsKey, "dataVersion", dataVersion);
|
||||
}
|
||||
|
||||
async function performClientMigrations() {
|
||||
let dataVersion = game.settings.get(settingsKey, "clientDataVersion");
|
||||
|
||||
if (dataVersion === "fresh install") {
|
||||
// Start of migration from unnamed version (< 1.10.0). TODO Remove in a future version
|
||||
const swapSpacebarRightClick = game.settings.storage
|
||||
.get("client")
|
||||
.getItem(`${settingsKey}.swapSpacebarRightClick`);
|
||||
if (swapSpacebarRightClick) {
|
||||
game.settings.set(settingsKey, "rightClickAction", RightClickAction.CREATE_WAYPOINT);
|
||||
await game.keybindings.set(settingsKey, "createWaypoint", []);
|
||||
await game.keybindings.set(settingsKey, "deleteWaypoint", [{key: "Space"}]);
|
||||
}
|
||||
// End of migration from unnamed version
|
||||
game.settings.set(settingsKey, "clientDataVersion", currentDataVersion);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import {measureDistances} from "./compatibility.js";
|
||||
import {recalculate, updateCombatantDragRulerFlags} from "./socket.js";
|
||||
import {getTokenShape, isClose, zip} from "./util.js";
|
||||
|
||||
function initTrackingFlag(combatant) {
|
||||
const initialFlag = {passedWaypoints: [], trackedRound: 0};
|
||||
let dragRulerFlag = combatant.data.flags.dragRuler;
|
||||
if (dragRulerFlag) {
|
||||
if (isNaN(dragRulerFlag.trackedRound)) {
|
||||
mergeObject(dragRulerFlag, initialFlag);
|
||||
}
|
||||
} else {
|
||||
combatant.data.flags.dragRuler = initialFlag;
|
||||
}
|
||||
}
|
||||
|
||||
function getInitializedCombatant(token, combat) {
|
||||
const combatant = combat.getCombatantByToken(token.id);
|
||||
if (!combatant) return undefined;
|
||||
initTrackingFlag(combatant);
|
||||
return combatant;
|
||||
}
|
||||
|
||||
export async function trackRays(tokens, tokenRays) {
|
||||
const combat = game.combat;
|
||||
if (!combat) return;
|
||||
if (!combat.started) return;
|
||||
if (!(tokens instanceof Array)) {
|
||||
tokens = [tokens];
|
||||
tokenRays = [tokenRays];
|
||||
}
|
||||
const updates = Array.from(zip(tokens, tokenRays))
|
||||
.map(([token, rays]) => calculateUpdate(combat, token, rays))
|
||||
.filter(Boolean);
|
||||
await updateCombatantDragRulerFlags(combat, updates);
|
||||
}
|
||||
|
||||
function calculateUpdate(combat, token, rays) {
|
||||
const combatant = getInitializedCombatant(token, combat);
|
||||
if (!combatant) return;
|
||||
|
||||
// Check if we have entered a new round. If so, remove the currently stored path
|
||||
if (combat.data.round > combatant.data.flags.dragRuler.trackedRound) {
|
||||
combatant.data.flags.dragRuler.passedWaypoints = [];
|
||||
combatant.data.flags.dragRuler.trackedRound = combat.data.round;
|
||||
}
|
||||
|
||||
// Add the passed waypoints to the combatant
|
||||
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active;
|
||||
const dragRulerFlags = combatant.data.flags.dragRuler;
|
||||
const waypoints = dragRulerFlags.passedWaypoints;
|
||||
for (const ray of rays) {
|
||||
// Ignore rays that have the same start and end coordinates
|
||||
if (ray.A.x !== ray.B.x || ray.A.y !== ray.B.y) {
|
||||
if (terrainRulerAvailable) {
|
||||
measureDistances([{ray}], token, getTokenShape(token), {
|
||||
terrainRulerInitialState: waypoints[waypoints.length - 1]?.dragRulerFinalState,
|
||||
enableTerrainRuler: terrainRulerAvailable,
|
||||
});
|
||||
ray.A.dragRulerVisitedSpaces = ray.terrainRulerVisitedSpaces;
|
||||
ray.A.dragRulerFinalState = ray.terrainRulerFinalState;
|
||||
}
|
||||
waypoints.push(ray.A);
|
||||
}
|
||||
}
|
||||
return {_id: combatant.id, dragRulerFlags};
|
||||
}
|
||||
|
||||
export function getMovementHistory(token) {
|
||||
const combat = game.combat;
|
||||
if (!combat) return [];
|
||||
const combatant = combat.getCombatantByToken(token.id);
|
||||
if (!combatant) return [];
|
||||
const dragRulerFlags = combatant.flags.dragRuler;
|
||||
if (!dragRulerFlags) return [];
|
||||
if (combat.round > dragRulerFlags.trackedRound) return [];
|
||||
return dragRulerFlags.passedWaypoints ?? [];
|
||||
}
|
||||
|
||||
export async function removeLastHistoryEntryIfAt(token, x, y) {
|
||||
const history = getMovementHistory(token);
|
||||
if (history.length === 0) return;
|
||||
const entry = history[history.length - 1];
|
||||
if (!isClose(x + token.w / 2, entry.x, 0.1) || !isClose(y + token.h / 2, entry.y, 0.1)) {
|
||||
return;
|
||||
}
|
||||
history.pop();
|
||||
const combat = game.combat;
|
||||
const combatant = combat.getCombatantByToken(token.id);
|
||||
await updateCombatantDragRulerFlags(combat, [
|
||||
{_id: combatant.id, dragRulerFlags: combatant.data.flags.dragRuler},
|
||||
]);
|
||||
}
|
||||
|
||||
export async function resetMovementHistory(combat, combatantId) {
|
||||
const combatant = combat.combatants.get(combatantId);
|
||||
const dragRulerFlags = combatant.data.flags.dragRuler;
|
||||
if (!dragRulerFlags) return;
|
||||
dragRulerFlags.passedWaypoints = null;
|
||||
dragRulerFlags.trackedRound = null;
|
||||
dragRulerFlags.rulerState = null;
|
||||
await updateCombatantDragRulerFlags(combat, [{_id: combatantId, dragRulerFlags}]);
|
||||
recalculate();
|
||||
}
|
||||
+495
@@ -0,0 +1,495 @@
|
||||
import {
|
||||
currentSpeedProvider,
|
||||
getColorForDistanceAndToken,
|
||||
getRangesFromSpeedProvider,
|
||||
} from "./api.js";
|
||||
import {
|
||||
getHexSizeSupportTokenGridCenter,
|
||||
highlightMeasurementTerrainRuler,
|
||||
measureDistances,
|
||||
} from "./compatibility.js";
|
||||
import {getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
|
||||
import {cancelScheduledMeasurement, highlightMeasurementNative} from "./foundry_imports.js";
|
||||
import {disableSnap} from "./keybindings.js";
|
||||
import {getMovementHistory} from "./movement_tracking.js";
|
||||
import {settingsKey} from "./settings.js";
|
||||
import {
|
||||
applyTokenSizeOffset,
|
||||
getSnapPointForEntity,
|
||||
getSnapPointForTokenObj,
|
||||
getTokenShape,
|
||||
isPathfindingEnabled,
|
||||
} from "./util.js";
|
||||
|
||||
export function extendRuler() {
|
||||
class DragRulerRuler extends CONFIG.Canvas.rulerClass {
|
||||
// Functions below are overridden versions of functions in Ruler
|
||||
constructor(user, {color = null} = {}) {
|
||||
super(user, {color});
|
||||
this.previousWaypoints = [];
|
||||
this.previousLabels = this.addChild(new PIXI.Container());
|
||||
}
|
||||
|
||||
clear() {
|
||||
super.clear();
|
||||
this.previousWaypoints = [];
|
||||
this.previousLabels.removeChildren().forEach(c => c.destroy());
|
||||
this.dragRulerRanges = undefined;
|
||||
cancelScheduledMeasurement.call(this);
|
||||
}
|
||||
|
||||
async moveToken(event) {
|
||||
// Disable moveToken if Drag Ruler is active
|
||||
if (!this.isDragRuler) return await super.moveToken(event);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const json = super.toJSON();
|
||||
if (this.draggedEntity) {
|
||||
const isToken = this.draggedEntity instanceof Token;
|
||||
json.draggedEntityIsToken = isToken;
|
||||
json.draggedEntity = this.draggedEntity.id;
|
||||
json.waypoints = json.waypoints.map(old => {
|
||||
let w = duplicate(old);
|
||||
w.isPathfinding = undefined;
|
||||
return w;
|
||||
});
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
update(data) {
|
||||
// Don't show a GMs drag ruler to non GM players
|
||||
if (
|
||||
data.draggedEntity &&
|
||||
this.user.isGM &&
|
||||
!game.user.isGM &&
|
||||
!game.settings.get(settingsKey, "showGMRulerToPlayers")
|
||||
)
|
||||
return;
|
||||
|
||||
if (data.draggedEntity) {
|
||||
if (data.draggedEntityIsToken) this.draggedEntity = canvas.tokens.get(data.draggedEntity);
|
||||
else this.draggedEntity = canvas.templates.get(data.draggedEntity);
|
||||
} else {
|
||||
this.draggedEntity = undefined;
|
||||
}
|
||||
|
||||
super.update(data);
|
||||
}
|
||||
|
||||
measure(destination, options = {}) {
|
||||
if (!this.isDragRuler) {
|
||||
return super.measure(destination, options);
|
||||
}
|
||||
if (options.gridSpaces === undefined) {
|
||||
options.gridSpaces = canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS;
|
||||
}
|
||||
this.dragRulerGridSpaces = options.gridSpaces;
|
||||
const isToken = this.draggedEntity instanceof Token;
|
||||
if (isToken && !this.draggedEntity.isVisible) {
|
||||
return [];
|
||||
}
|
||||
if (canvas.grid.diagonalRule === "EUCL") {
|
||||
options.gridSpaces = false;
|
||||
options.ignoreGrid = true;
|
||||
}
|
||||
if (options.ignoreGrid === undefined) {
|
||||
options.ignoreGrid = false;
|
||||
}
|
||||
this.dragRulerIgnoreGrid = options.ignoreGrid;
|
||||
// If this is the ruler of a remote user take the waypoints as they were transmitted and don't apply any additional snapping to them
|
||||
if (this.user !== game.user) {
|
||||
options.snap = false;
|
||||
}
|
||||
this.dragRulerSnap = options.snap ?? !disableSnap;
|
||||
this.dragRulerEnableTerrainRuler = isToken && window.terrainRuler;
|
||||
|
||||
// Compute the measurement destination, segments, and distance
|
||||
const d = this._getMeasurementDestination(destination);
|
||||
if (d.x === this.destination.x && d.y === this.destination.y) return;
|
||||
this.destination = d;
|
||||
|
||||
// TODO Cancel running pathfinding operations
|
||||
// TODO Check if we can reuse the old path
|
||||
this.dragRulerRemovePathfindingWaypoints();
|
||||
|
||||
if (isToken && isPathfindingEnabled.call(this)) {
|
||||
// TODO Show a busy indicator
|
||||
const from = getGridPositionFromPixelsObj(this.waypoints[this.waypoints.length - 1]);
|
||||
const to = getGridPositionFromPixelsObj(destination);
|
||||
|
||||
return routinglib
|
||||
.calculatePath(from, to, {token: this.draggedEntity})
|
||||
.then(result => this.addPathToWaypoints(result.path))
|
||||
.then(() => this.performPostPathfindingActions(options));
|
||||
}
|
||||
|
||||
return this.performPostPathfindingActions(options);
|
||||
}
|
||||
|
||||
addPathToWaypoints(path) {
|
||||
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 (this.dragRulerSnap) {
|
||||
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);
|
||||
}
|
||||
|
||||
performPostPathfindingActions(options) {
|
||||
// TODO Clear pathfinding busy indicator
|
||||
this.segments = this._getMeasurementSegments();
|
||||
this._computeDistance(options.gridSpaces);
|
||||
|
||||
// Draw the ruler graphic
|
||||
this.ruler.clear();
|
||||
this._drawMeasuredPath();
|
||||
|
||||
// Draw grid highlight
|
||||
this.highlightLayer.clear();
|
||||
const isToken = this.draggedEntity instanceof Token;
|
||||
if (isToken && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS && this.dragRulerGridSpaces) {
|
||||
const shape = getTokenShape(this.draggedEntity);
|
||||
if (!this.dragRulerEnableTerrainRuler) {
|
||||
for (const [i, segment] of [...this.segments].reverse().entries()) {
|
||||
const opacityMultiplier = segment.ray.isPrevious ? 0.33 : 1;
|
||||
const previousSegments = this.segments.slice(0, this.segments.length - 1 - i);
|
||||
highlightMeasurementNative.call(
|
||||
this,
|
||||
segment.ray,
|
||||
previousSegments,
|
||||
shape,
|
||||
opacityMultiplier,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for (const segment of [...this.segments].reverse()) {
|
||||
const opacityMultiplier = segment.ray.isPrevious ? 0.33 : 1;
|
||||
highlightMeasurementTerrainRuler.call(
|
||||
this,
|
||||
segment.ray,
|
||||
segment.startDistance,
|
||||
shape,
|
||||
opacityMultiplier,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.segments;
|
||||
}
|
||||
|
||||
_getMeasurementDestination(destination) {
|
||||
if (this.isDragRuler) {
|
||||
if (this.dragRulerSnap) {
|
||||
return getSnapPointForEntity(destination.x, destination.y, this.draggedEntity);
|
||||
} else {
|
||||
return destination;
|
||||
}
|
||||
} else {
|
||||
return super._getMeasurementDestination(destination);
|
||||
}
|
||||
}
|
||||
|
||||
_getMeasurementSegments() {
|
||||
if (this.isDragRuler) {
|
||||
const unsnappedWaypoints = this.waypoints.concat([this.destination]);
|
||||
const waypoints =
|
||||
this.draggedEntity instanceof Token
|
||||
? applyTokenSizeOffset(unsnappedWaypoints, this.draggedEntity)
|
||||
: duplicate(unsnappedWaypoints);
|
||||
const unsnappedSegments = [];
|
||||
const segments = [];
|
||||
for (const [i, p1] of waypoints.entries()) {
|
||||
if (i === 0) continue;
|
||||
const unsnappedP1 = unsnappedWaypoints[i];
|
||||
const p0 = waypoints[i - 1];
|
||||
const unsnappedP0 = unsnappedWaypoints[i - 1];
|
||||
const label = this.labels.children[i - 1];
|
||||
const ray = new Ray(p0, p1);
|
||||
const unsnappedRay = new Ray(unsnappedP0, unsnappedP1);
|
||||
ray.isPrevious = Boolean(unsnappedP0.isPrevious);
|
||||
unsnappedRay.isPrevious = ray.isPrevious;
|
||||
ray.dragRulerVisitedSpaces = unsnappedP0.dragRulerVisitedSpaces;
|
||||
unsnappedRay.dragRulerVisitedSpaces = ray.dragRulerVisitedSpaces;
|
||||
ray.dragRulerFinalState = unsnappedP0.dragRulerFinalState;
|
||||
unsnappedRay.dragRulerFinalState = ray.dragRulerFinalState;
|
||||
if (ray.distance < 10) {
|
||||
if (label) label.visible = false;
|
||||
continue;
|
||||
}
|
||||
segments.push({ray, label});
|
||||
unsnappedSegments.push({ray: unsnappedRay, label});
|
||||
}
|
||||
this.dragRulerUnsnappedSegments = unsnappedSegments;
|
||||
return segments;
|
||||
} else {
|
||||
return super._getMeasurementSegments();
|
||||
}
|
||||
}
|
||||
|
||||
_computeDistance(gridSpaces) {
|
||||
if (!this.isDragRuler) {
|
||||
return super._computeDistance(gridSpaces);
|
||||
}
|
||||
if (!this.dragRulerEnableTerrainRuler) {
|
||||
if (!this.dragRulerIgnoreGrid) {
|
||||
gridSpaces = true;
|
||||
}
|
||||
super._computeDistance(gridSpaces);
|
||||
} else {
|
||||
const shape = this.draggedEntity ? getTokenShape(this.draggedEntity) : null;
|
||||
const options = {
|
||||
ignoreGrid: this.dragRulerIgnoreGrid,
|
||||
gridSpaces,
|
||||
enableTerrainRuler: this.dragRulerEnableTerrainRuler,
|
||||
};
|
||||
const distances = measureDistances(this.segments, this.draggedEntity, shape, options);
|
||||
let totalDistance = 0;
|
||||
for (const [i, d] of distances.entries()) {
|
||||
let s = this.segments[i];
|
||||
s.startDistance = totalDistance;
|
||||
totalDistance += d;
|
||||
s.last = i === this.segments.length - 1;
|
||||
s.distance = d;
|
||||
s.text = this._getSegmentLabel(s, totalDistance);
|
||||
}
|
||||
}
|
||||
for (const [i, segment] of this.segments.entries()) {
|
||||
const unsnappedSegment = this.dragRulerUnsnappedSegments[i];
|
||||
unsnappedSegment.startDistance = segment.startDistance;
|
||||
unsnappedSegment.last = segment.last;
|
||||
unsnappedSegment.distance = segment.distance;
|
||||
unsnappedSegment.text = segment.text;
|
||||
}
|
||||
}
|
||||
|
||||
_drawMeasuredPath() {
|
||||
if (!this.isDragRuler) {
|
||||
return super._drawMeasuredPath();
|
||||
}
|
||||
let rulerColor = this.color;
|
||||
if (!this.dragRulerGridSpaces || canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||
const totalDistance = this.segments.reduce((total, current) => total + current.distance, 0);
|
||||
rulerColor = this.dragRulerGetColorForDistance(totalDistance);
|
||||
}
|
||||
const r = this.ruler.beginFill(rulerColor, 0.25);
|
||||
for (const segment of this.dragRulerUnsnappedSegments) {
|
||||
const opacityMultiplier = segment.ray.isPrevious ? 0.33 : 1;
|
||||
const {ray, distance, label, text, last} = segment;
|
||||
if (distance === 0) continue;
|
||||
|
||||
// Draw Line
|
||||
r.moveTo(ray.A.x, ray.A.y)
|
||||
.lineStyle(6, 0x000000, 0.5 * opacityMultiplier)
|
||||
.lineTo(ray.B.x, ray.B.y)
|
||||
.lineStyle(4, rulerColor, 0.25 * opacityMultiplier)
|
||||
.moveTo(ray.A.x, ray.A.y)
|
||||
.lineTo(ray.B.x, ray.B.y);
|
||||
|
||||
// Draw Waypoints
|
||||
r.lineStyle(2, 0x000000, 0.5).drawCircle(ray.A.x, ray.A.y, 8);
|
||||
if (last) r.drawCircle(ray.B.x, ray.B.y, 8);
|
||||
|
||||
// Draw Label
|
||||
if (label) {
|
||||
label.text = text;
|
||||
label.alpha = last ? 1.0 : 0.5;
|
||||
label.visible = true;
|
||||
let labelPosition = ray.project((ray.distance + 50) / ray.distance);
|
||||
label.position.set(labelPosition.x, labelPosition.y);
|
||||
}
|
||||
}
|
||||
r.endFill();
|
||||
}
|
||||
|
||||
_endMeasurement() {
|
||||
super._endMeasurement();
|
||||
this.draggedEntity = null;
|
||||
}
|
||||
|
||||
// The functions below aren't present in the orignal Ruler class and are added by Drag Ruler
|
||||
dragRulerAddWaypoint(point, options = {}) {
|
||||
options.snap = options.snap ?? true;
|
||||
if (options.snap) {
|
||||
point = getSnapPointForEntity(point.x, point.y, this.draggedEntity);
|
||||
}
|
||||
this.waypoints.push(new PIXI.Point(point.x, point.y));
|
||||
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
|
||||
this.waypoints
|
||||
.filter(waypoint => waypoint.isPathfinding)
|
||||
.forEach(waypoint => (waypoint.isPathfinding = false));
|
||||
}
|
||||
|
||||
dragRulerAddWaypointHistory(waypoints) {
|
||||
waypoints.forEach(waypoint => (waypoint.isPrevious = true));
|
||||
this.waypoints = this.waypoints.concat(waypoints);
|
||||
for (const waypoint of waypoints) {
|
||||
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
|
||||
}
|
||||
}
|
||||
|
||||
dragRulerClearWaypoints() {
|
||||
this.waypoints = [];
|
||||
this.labels.removeChildren().forEach(c => c.destroy());
|
||||
}
|
||||
|
||||
dragRulerDeleteWaypoint(
|
||||
event = {
|
||||
preventDefault: () => {
|
||||
return;
|
||||
},
|
||||
},
|
||||
options = {},
|
||||
) {
|
||||
this.dragRulerRemovePathfindingWaypoints();
|
||||
options.snap = options.snap ?? true;
|
||||
if (this.waypoints.filter(w => !w.isPrevious).length > 1) {
|
||||
event.preventDefault();
|
||||
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(
|
||||
canvas.tokens,
|
||||
);
|
||||
const rulerOffset = this.rulerOffset;
|
||||
|
||||
// Options are not passed to _removeWaypoint in vanilla Foundry.
|
||||
// Send them in case other modules have overriden that behavior and accept an options parameter (Toggle Snap to Grid)
|
||||
this._removeWaypoint(
|
||||
{x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y},
|
||||
options,
|
||||
);
|
||||
game.user.broadcastActivity({ruler: this});
|
||||
} else {
|
||||
this.dragRulerAbortDrag(event);
|
||||
}
|
||||
}
|
||||
|
||||
dragRulerRemovePathfindingWaypoints() {
|
||||
this.waypoints
|
||||
.filter(waypoint => waypoint.isPathfinding)
|
||||
.forEach(_ =>
|
||||
this.labels.removeChild(this.labels.children[this.labels.children.length - 1]).destroy(),
|
||||
);
|
||||
this.waypoints = this.waypoints.filter(waypoint => !waypoint.isPathfinding);
|
||||
}
|
||||
|
||||
dragRulerAbortDrag(
|
||||
event = {
|
||||
preventDefault: () => {
|
||||
return;
|
||||
},
|
||||
},
|
||||
) {
|
||||
const token = this.draggedEntity;
|
||||
this._endMeasurement();
|
||||
|
||||
// Deactivate the drag workflow in mouse
|
||||
token.mouseInteractionManager._deactivateDragEvents();
|
||||
token.mouseInteractionManager.state = token.mouseInteractionManager.states.HOVER;
|
||||
|
||||
// This will cancel the current drag operation
|
||||
// Pass in a fake event that hopefully is enough to allow other modules to function
|
||||
token._onDragLeftCancel(event);
|
||||
}
|
||||
|
||||
async dragRulerRecalculate(tokenIds) {
|
||||
if (this._state !== Ruler.STATES.MEASURING) return;
|
||||
if (tokenIds && !tokenIds.includes(this.draggedEntity.id)) return;
|
||||
const waypoints = this.waypoints.filter(waypoint => !waypoint.isPrevious);
|
||||
this.dragRulerClearWaypoints();
|
||||
if (game.settings.get(settingsKey, "enableMovementHistory"))
|
||||
this.dragRulerAddWaypointHistory(getMovementHistory(this.draggedEntity));
|
||||
for (const waypoint of waypoints) {
|
||||
this.dragRulerAddWaypoint(waypoint, {snap: false});
|
||||
}
|
||||
this.measure(this.destination);
|
||||
game.user.broadcastActivity({ruler: this});
|
||||
}
|
||||
|
||||
static dragRulerGetRaysFromWaypoints(waypoints, destination) {
|
||||
if (destination) waypoints = waypoints.concat([destination]);
|
||||
return waypoints.slice(1).map((wp, i) => {
|
||||
const ray = new Ray(waypoints[i], wp);
|
||||
ray.isPrevious = Boolean(waypoints[i].isPrevious);
|
||||
return ray;
|
||||
});
|
||||
}
|
||||
|
||||
dragRulerGetColorForDistance(distance) {
|
||||
if (!this.isDragRuler) return this.color;
|
||||
if (!this.draggedEntity.actor) {
|
||||
return this.color;
|
||||
}
|
||||
// Don't apply colors if the current user doesn't have at least observer permissions
|
||||
if (this.draggedEntity.actor.permission < 2) {
|
||||
// If this is a pc and alwaysShowSpeedForPCs is enabled we show the color anyway
|
||||
if (
|
||||
!(
|
||||
this.draggedEntity.actor.data.type === "character" &&
|
||||
game.settings.get(settingsKey, "alwaysShowSpeedForPCs")
|
||||
)
|
||||
)
|
||||
return this.color;
|
||||
}
|
||||
distance = Math.round(distance * 100) / 100;
|
||||
if (!this.dragRulerRanges)
|
||||
this.dragRulerRanges = getRangesFromSpeedProvider(this.draggedEntity);
|
||||
return (
|
||||
getColorForDistanceAndToken(distance, this.draggedEntity, this.dragRulerRanges) ??
|
||||
this.color
|
||||
);
|
||||
}
|
||||
|
||||
dragRulerStart(options, measureImmediately = true) {
|
||||
const entity = this.draggedEntity;
|
||||
const isToken = entity instanceof Token;
|
||||
if (isToken && !currentSpeedProvider.usesRuler(entity)) return;
|
||||
const ruler = canvas.controls.ruler;
|
||||
ruler.clear();
|
||||
ruler._state = Ruler.STATES.STARTING;
|
||||
let entityCenter;
|
||||
if (
|
||||
isToken &&
|
||||
canvas.grid.isHex &&
|
||||
game.modules.get("hex-size-support")?.active &&
|
||||
CONFIG.hexSizeSupport.getAltSnappingFlag(entity)
|
||||
)
|
||||
entityCenter = getHexSizeSupportTokenGridCenter(entity);
|
||||
else entityCenter = entity.center;
|
||||
if (isToken && game.settings.get(settingsKey, "enableMovementHistory"))
|
||||
ruler.dragRulerAddWaypointHistory(getMovementHistory(entity));
|
||||
ruler.dragRulerAddWaypoint(entityCenter, {snap: false});
|
||||
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(
|
||||
canvas.tokens,
|
||||
);
|
||||
const destination = {
|
||||
x: mousePosition.x + ruler.rulerOffset.x,
|
||||
y: mousePosition.y + ruler.rulerOffset.y,
|
||||
};
|
||||
if (measureImmediately) ruler.measure(destination, options);
|
||||
}
|
||||
|
||||
dragRulerSendState() {
|
||||
game.user.broadcastActivity({
|
||||
ruler: this.toJSON(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
CONFIG.Canvas.rulerClass = DragRulerRuler;
|
||||
}
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
import {
|
||||
availableSpeedProviders,
|
||||
currentSpeedProvider,
|
||||
getDefaultSpeedProvider,
|
||||
updateSpeedProvider,
|
||||
} from "./api.js";
|
||||
import {SpeedProvider} from "./speed_provider.js";
|
||||
import {early_isGM} from "./util.js";
|
||||
|
||||
export const settingsKey = "drag-ruler";
|
||||
|
||||
export const RightClickAction = Object.freeze({
|
||||
CREATE_WAYPOINT: 0,
|
||||
DELETE_WAYPOINT: 1,
|
||||
ABORT_DRAG: 2,
|
||||
});
|
||||
|
||||
function delayedReload() {
|
||||
window.setTimeout(() => location.reload(), 500);
|
||||
}
|
||||
|
||||
export function registerSettings() {
|
||||
game.settings.register(settingsKey, "dataVersion", {
|
||||
scope: "world",
|
||||
config: false,
|
||||
type: String,
|
||||
default: "fresh install",
|
||||
});
|
||||
|
||||
game.settings.register(settingsKey, "clientDataVersion", {
|
||||
scope: "client",
|
||||
config: false,
|
||||
type: String,
|
||||
default: "fresh install",
|
||||
});
|
||||
|
||||
game.settings.register(settingsKey, "rightClickAction", {
|
||||
name: "drag-ruler.settings.rightClickAction.name",
|
||||
hint: "drag-ruler.settings.rightClickAction.hint",
|
||||
config: true,
|
||||
type: Number,
|
||||
default: RightClickAction.DELETE_WAYPOINT,
|
||||
choices: {
|
||||
0: "drag-ruler.settings.rightClickAction.choices.create",
|
||||
1: "drag-ruler.settings.rightClickAction.choices.delete",
|
||||
2: "drag-ruler.settings.rightClickAction.choices.cancel",
|
||||
},
|
||||
});
|
||||
|
||||
game.settings.register(settingsKey, "autoStartMeasurement", {
|
||||
name: "drag-ruler.settings.autoStartMeasurement.name",
|
||||
hint: "drag-ruler.settings.autoStartMeasurement.hint",
|
||||
scope: "client",
|
||||
config: true,
|
||||
type: Boolean,
|
||||
default: true,
|
||||
});
|
||||
|
||||
game.settings.register(settingsKey, "useGridlessRaster", {
|
||||
name: "drag-ruler.settings.useGridlessRaster.name",
|
||||
hint: "drag-ruler.settings.useGridlessRaster.hint",
|
||||
scope: "client",
|
||||
config: true,
|
||||
type: Boolean,
|
||||
default: true,
|
||||
});
|
||||
|
||||
game.settings.register(settingsKey, "alwaysShowSpeedForPCs", {
|
||||
name: "drag-ruler.settings.alwaysShowSpeedForPCs.name",
|
||||
hint: "drag-ruler.settings.alwaysShowSpeedForPCs.hint",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: Boolean,
|
||||
default: true,
|
||||
});
|
||||
|
||||
game.settings.register(settingsKey, "showGMRulerToPlayers", {
|
||||
name: "drag-ruler.settings.showGMRulerToPlayers.name",
|
||||
hint: "drag-ruler.settings.showGMRulerToPlayers.hint",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: Boolean,
|
||||
default: true,
|
||||
});
|
||||
|
||||
game.settings.register(settingsKey, "enableMovementHistory", {
|
||||
name: "drag-ruler.settings.enableMovementHistory.name",
|
||||
hint: "drag-ruler.settings.enableMovementHistory.hint",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: Boolean,
|
||||
default: true,
|
||||
});
|
||||
|
||||
if (game.modules.get("routinglib")?.active) {
|
||||
game.settings.register(settingsKey, "allowPathfinding", {
|
||||
name: "drag-ruler.settings.allowPathfinding.name",
|
||||
hint: "drag-ruler.settings.allowPathfinding.hint",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
onChange: delayedReload,
|
||||
});
|
||||
|
||||
game.settings.register(settingsKey, "autoPathfinding", {
|
||||
name: "drag-ruler.settings.autoPathfinding.name",
|
||||
hint: "drag-ruler.settings.autoPathfinding.hint",
|
||||
scope: "client",
|
||||
config: early_isGM() || game.settings.get(settingsKey, "allowPathfinding"),
|
||||
type: Boolean,
|
||||
default: false,
|
||||
});
|
||||
}
|
||||
|
||||
game.settings.register(settingsKey, "lastTerrainRulerHintTime", {
|
||||
config: false,
|
||||
type: Number,
|
||||
default: 0,
|
||||
});
|
||||
|
||||
game.settings.register(settingsKey, "neverShowTerrainRulerHint", {
|
||||
config: false,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
});
|
||||
|
||||
// This setting will be modified by the api if modules register to it
|
||||
game.settings.register(settingsKey, "speedProvider", {
|
||||
scope: "world",
|
||||
config: false,
|
||||
type: String,
|
||||
default: getDefaultSpeedProvider(),
|
||||
onChange: updateSpeedProvider,
|
||||
});
|
||||
|
||||
game.settings.registerMenu(settingsKey, "speedProviderSettings", {
|
||||
name: "drag-ruler.settings.speedProviderSettings.name",
|
||||
hint: "drag-ruler.settings.speedProviderSettings.hint",
|
||||
label: "drag-ruler.settings.speedProviderSettings.button",
|
||||
icon: "fas fa-tachometer-alt",
|
||||
type: SpeedProviderSettings,
|
||||
restricted: false,
|
||||
});
|
||||
}
|
||||
|
||||
class SpeedProviderSettings extends FormApplication {
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
id: "drag-ruler-speed-provider-settings",
|
||||
title: game.i18n.localize("drag-ruler.settings.speedProviderSettings.windowTitle"),
|
||||
template: "modules/drag-ruler/templates/speed_provider_settings.html",
|
||||
width: 600,
|
||||
});
|
||||
}
|
||||
|
||||
getData(options = {}) {
|
||||
const data = {};
|
||||
data.isGM = game.user.isGM;
|
||||
const selectedProvider = currentSpeedProvider.id;
|
||||
|
||||
// Insert all speed providers into the template data
|
||||
data.providers = Object.values(availableSpeedProviders).map(speedProvider => {
|
||||
const provider = {};
|
||||
provider.id = speedProvider.id;
|
||||
provider.hasSettings = speedProvider instanceof SpeedProvider;
|
||||
if (provider.hasSettings) provider.settings = enumerateProviderSettings(speedProvider);
|
||||
let dotPosition = provider.id.indexOf(".");
|
||||
if (dotPosition === -1) dotPosition = provider.id.length;
|
||||
const type = provider.id.substring(0, dotPosition);
|
||||
const id = provider.id.substring(dotPosition + 1);
|
||||
if (type === "native") {
|
||||
provider.selectTitle = game.i18n.localize(
|
||||
"drag-ruler.settings.speedProviderSettings.speedProvider.choices.native",
|
||||
);
|
||||
} else {
|
||||
let name;
|
||||
if (type === "module") {
|
||||
name = game.modules.get(id).data.title;
|
||||
} else {
|
||||
name = game.system.data.title;
|
||||
}
|
||||
provider.selectTitle = game.i18n.format(
|
||||
`drag-ruler.settings.speedProviderSettings.speedProvider.choices.${type}`,
|
||||
{name},
|
||||
);
|
||||
}
|
||||
provider.isSelected = provider.id === selectedProvider;
|
||||
return provider;
|
||||
});
|
||||
data.selectedProviderName = data.providers.find(provider => provider.isSelected).selectTitle;
|
||||
|
||||
data.providerSelection = {
|
||||
id: "speedProvider",
|
||||
name: game.i18n.localize("drag-ruler.settings.speedProviderSettings.speedProvider.name"),
|
||||
hint: game.i18n.localize("drag-ruler.settings.speedProviderSettings.speedProvider.hint"),
|
||||
type: String,
|
||||
choices: data.providers.reduce((choices, provider) => {
|
||||
choices[provider.id] = provider.selectTitle;
|
||||
return choices;
|
||||
}, {}),
|
||||
value: selectedProvider,
|
||||
isCheckbox: false,
|
||||
isSelect: true,
|
||||
isRange: false,
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
async _updateObject(event, formData) {
|
||||
const selectedSpeedProvider = game.user.isGM
|
||||
? formData.speedProvider
|
||||
: game.settings.get(settingsKey, "speedProvider");
|
||||
for (let [key, value] of Object.entries(formData)) {
|
||||
// Check if this is color, convert the value to an integer
|
||||
const splitKey = key.split(".", 3);
|
||||
if (splitKey[0] !== "native") splitKey.shift();
|
||||
if (splitKey.length >= 2 && splitKey[1] == "color") {
|
||||
value = parseInt(value.substring(1), 16);
|
||||
}
|
||||
|
||||
// Don't change settings for speed providers that aren't currently active
|
||||
if (key !== "speedProvider" && !key.startsWith(selectedSpeedProvider)) continue;
|
||||
|
||||
// Get the key for the current setting
|
||||
let setting;
|
||||
if (key === "speedProvider") setting = "speedProvider";
|
||||
else setting = `speedProviders.${key}`;
|
||||
|
||||
// Get the old setting value
|
||||
const oldValue = game.settings.get(settingsKey, setting);
|
||||
|
||||
// Only update the setting if it has been changed (this leaves the default in place if it hasn't been touched)
|
||||
if (value !== oldValue) game.settings.set(settingsKey, setting, value);
|
||||
}
|
||||
|
||||
// Activate the configured speed provider
|
||||
updateSpeedProvider();
|
||||
}
|
||||
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
html.find("select[name=speedProvider]").change(this.onSpeedProviderChange.bind(this));
|
||||
}
|
||||
|
||||
onSpeedProviderChange(event) {
|
||||
// Hide all module settings
|
||||
document
|
||||
.querySelectorAll(".drag-ruler-provider-settings")
|
||||
.forEach(element => (element.style.display = "none"));
|
||||
// Show the settings block for the currently selected module
|
||||
document.getElementById(`drag-ruler.provider.${event.currentTarget.value}`).style.display = "";
|
||||
|
||||
// Recalculate window height
|
||||
this.element[0].style.height = null;
|
||||
this.position.height = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function toDomHex(value) {
|
||||
const hex = value.toString(16);
|
||||
return "#" + "0".repeat(Math.max(0, 6 - hex.length)) + hex;
|
||||
}
|
||||
|
||||
function enumerateProviderSettings(provider) {
|
||||
const colorSettings = [];
|
||||
const unreachableColor = {
|
||||
id: "unreachable",
|
||||
name: "drag-ruler.settings.speedProviderSettings.color.unreachable.name",
|
||||
};
|
||||
|
||||
// Resolve settings for the colors
|
||||
for (const color of provider.colors.concat([unreachableColor])) {
|
||||
// Localize the name, if avaliable. If no name is available use the id as name
|
||||
const colorName = color.name ? game.i18n.localize(color.name) : color.id;
|
||||
let hint;
|
||||
if (color === unreachableColor)
|
||||
hint = game.i18n.localize("drag-ruler.settings.speedProviderSettings.color.unreachable.hint");
|
||||
else
|
||||
hint = game.i18n.format("drag-ruler.settings.speedProviderSettings.color.hint", {colorName});
|
||||
colorSettings.push({
|
||||
id: `${provider.id}.color.${color.id}`,
|
||||
name: game.i18n.format("drag-ruler.settings.speedProviderSettings.color.name", {colorName}),
|
||||
hint: hint,
|
||||
type: Number,
|
||||
value: toDomHex(
|
||||
game.settings.get(settingsKey, `speedProviders.${provider.id}.color.${color.id}`),
|
||||
),
|
||||
isCheckbox: false,
|
||||
isSelect: false,
|
||||
isRange: false,
|
||||
isColor: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Prepare regular settings
|
||||
const settings = [];
|
||||
for (const setting of provider.settings) {
|
||||
try {
|
||||
if (setting.scope === "world" && !game.user.isGM) continue;
|
||||
const s = duplicate(setting);
|
||||
s.id = `${provider.id}.setting.${s.id}`;
|
||||
s.name = game.i18n.localize(s.name);
|
||||
s.hint = game.i18n.localize(s.hint);
|
||||
s.value = provider.getSetting(setting.id);
|
||||
s.type = setting.type instanceof Function ? setting.type.name : "String";
|
||||
s.isCheckbox = setting.type === Boolean;
|
||||
s.isSelect = s.choices !== undefined;
|
||||
s.isRange = setting.type === Number && s.range;
|
||||
s.isColor = false;
|
||||
settings.push(s);
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
`Drag Ruler | The following error occured while rendering setting "${setting.id}" of module/system "${this.id}. It won't be displayed.`,
|
||||
);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return settings.concat(colorSettings);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import {currentSpeedProvider} from "./api.js";
|
||||
|
||||
let socket;
|
||||
|
||||
Hooks.once("socketlib.ready", () => {
|
||||
socket = socketlib.registerModule("drag-ruler");
|
||||
socket.register("updateCombatantDragRulerFlags", _socketUpdateCombatantDragRulerFlags);
|
||||
socket.register("recalculate", _socketRecalculate);
|
||||
});
|
||||
|
||||
export function updateCombatantDragRulerFlags(combat, updates) {
|
||||
const combatId = combat.id;
|
||||
// TODO Check if canvas.tokens.get is still neccessary in future foundry versions
|
||||
return socket
|
||||
.executeAsGM(_socketUpdateCombatantDragRulerFlags, combatId, updates)
|
||||
.then(() =>
|
||||
currentSpeedProvider.onMovementHistoryUpdate(
|
||||
updates.map(update => canvas.tokens.get(combat.combatants.get(update._id).token.id)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function _socketUpdateCombatantDragRulerFlags(combatId, updates) {
|
||||
const user = game.users.get(this.socketdata.userId);
|
||||
const combat = game.combats.get(combatId);
|
||||
const requestedUpdates = updates.length;
|
||||
updates = updates.filter(update => {
|
||||
const actor = combat.combatants.get(update._id).actor;
|
||||
if (!actor) return false;
|
||||
return actor.testUserPermission(user, "OWNER");
|
||||
});
|
||||
if (updates.length !== requestedUpdates) {
|
||||
console.warn(
|
||||
`Some of the movement history updates requested by user '${
|
||||
game.users.get(this.socketdata.userId).name
|
||||
}' were not performed because the user lacks owner permissions for those tokens`,
|
||||
);
|
||||
}
|
||||
updates = updates.map(update => {
|
||||
return {_id: update._id, flags: {dragRuler: update.dragRulerFlags}};
|
||||
});
|
||||
await combat.updateEmbeddedDocuments("Combatant", updates, {diff: false});
|
||||
}
|
||||
|
||||
export function recalculate(tokens) {
|
||||
socket.executeForEveryone(_socketRecalculate, tokens ? tokens.map(token => token.id) : undefined);
|
||||
}
|
||||
|
||||
function _socketRecalculate(tokenIds) {
|
||||
const ruler = canvas.controls.ruler;
|
||||
if (ruler.isDragRuler) ruler.dragRulerRecalculate(tokenIds);
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import {settingsKey} from "./settings.js";
|
||||
import {getDefaultDashMultiplier, getDefaultSpeedAttribute} from "./systems.js";
|
||||
|
||||
/**
|
||||
* Base class for all speed providers.
|
||||
* If you want to offer a speed provider in your system/module you must derive this class.
|
||||
* Each speed provider must at least implement
|
||||
*/
|
||||
export class SpeedProvider {
|
||||
/**
|
||||
* Returns an array of colors used by this speed provider. Each color corresponds to one speed that a token may have.
|
||||
* Each color must be an object with the following properties:
|
||||
* - id: A value that identfies the color. Must be unique for each color returned.
|
||||
* - default: The color that is used to highlight that speed by default.
|
||||
* - name: A user readable name for the speed represented by the color. This name is used in the color configuration dialog. Drag Ruler will attempt to localize this string using `game.i18n`
|
||||
*
|
||||
* Of these properties, id and defaultColor are required. name is optional, but it's recommended to set it
|
||||
*
|
||||
* Implementing this method is required for all speed providers
|
||||
*/
|
||||
get colors() {
|
||||
throw new Error("A SpeedProvider must implement the colors function");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of speeds that the token passed in the arguments this token can reach.
|
||||
* Each range is an object that with the following properties:
|
||||
* - range: A number indicating the distance that the token can travel with this speed
|
||||
* - color: The id (as defined in the `colors` getter) of the color that should be used to represent this range
|
||||
*
|
||||
* Implementing this method is required for all speed providers
|
||||
*/
|
||||
getRanges(token) {
|
||||
throw new Error("A SpeedProvider must implement the getRanges function");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of configuration options for this module. The settings will be shown in the Speed Provider Settings of Drag Ruler.
|
||||
* Each configuration option is an object that has the same attributes as a native foundry setting passed to `game.settings.register`,
|
||||
* except for these exceptions:
|
||||
* - id: A string that identifies the setting. Must be unique for each setting returned. This id will be used to fetch the setting.
|
||||
* - config: This property is not supported by Drag Ruler module settings. Use foundries native settings instead if you need settings that don't show up in the configuration dialog.
|
||||
*
|
||||
* Implementing this method is optional and only needs to be done if you want to provide custom provider settings
|
||||
*/
|
||||
get settings() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default color for ranges that a token cannot reach.
|
||||
*
|
||||
* Implementing this method is optional and only needs to be done if you want to provide a custom default for that color.
|
||||
*/
|
||||
get defaultUnreachableColor() {
|
||||
return 0xff0000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cost for a token to step into the specificed area.
|
||||
* The area indicates the whole area that the token will occupy (for tokens larger than 1x1) the array will more than one entry.
|
||||
* The return value should be an integer indicating a multiplicator by that the cost of that step should be increased.
|
||||
* (1 is regular cost, 2 costs double, 3 costs triple, ...)
|
||||
*
|
||||
* Parameters:
|
||||
* - options: An object used to configure Enhanced Terrain Layer's cost calculation. Ex: If options.ignoreGrid is set to true, then Euclidean measurement can be forced on a gridded map.
|
||||
*
|
||||
* This function is only called if the Enhanced Terrain Layer and Terrain Ruler modules are enabled.
|
||||
*
|
||||
* Implementing this method is optional and only needs to be done if you want to provide a custom cost function (for example to allow tokens to ignore difficult terrain)
|
||||
*/
|
||||
getCostForStep(token, area, options = {}) {
|
||||
// Lookup the cost for each square occupied by the token
|
||||
options.token = token;
|
||||
const costs = area.map(space => terrainRuler.getCost(space.x, space.y, options));
|
||||
// Return the maximum of the costs
|
||||
return costs.reduce((max, current) => Math.max(max, current));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean indicating whether this token will use a Ruler or not.
|
||||
* If this is returns `false` for a token Drag Ruler will be disabled for that token. Dragging a token for which this function
|
||||
* returns false will behave as if Drag Ruler wasn't installed.
|
||||
* If usesRuler returns `false` it's guranteed that the `getRanges` function won't be called for that token.
|
||||
*
|
||||
* Implementing this method is optional and only needs to be done if you want to disable Drag Ruler for some tokens.
|
||||
*/
|
||||
usesRuler(token) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook is being called after Drag Ruler has updated the movement history for one or more tokens.
|
||||
* It'll receive an array of tokens that have been updated.
|
||||
* If your speed provider is storing any additional values that are relevant for the movement history, this function should
|
||||
* await until those updates have completed inside foundry.
|
||||
*/
|
||||
async onMovementHistoryUpdate(tokens) {}
|
||||
|
||||
/**
|
||||
* Returns the value that is currently set for the setting registered with the provided settingId.
|
||||
*
|
||||
* This function shouldn't be overridden by speed provider implementations. It can be called to fetch speed provider specific settings.
|
||||
*/
|
||||
getSetting(settingId) {
|
||||
try {
|
||||
return game.settings.get(settingsKey, `speedProviders.${this.id}.setting.${settingId}`);
|
||||
} catch (e) {
|
||||
if (this.settings.some(setting => setting.id === settingId)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error(
|
||||
`Drag Ruler | "${settingId}" is not a registered setting for "${this.id}". If you're the module/system developer, please add it to the return values of your Speed Providers "get settings()" function.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new instance of he speed provider
|
||||
*
|
||||
* This function should neither be called or overridden by speed provider implementations
|
||||
*/
|
||||
constructor(id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
export class GenericSpeedProvider extends SpeedProvider {
|
||||
get colors() {
|
||||
return [
|
||||
{id: "walk", default: 0x00ff00, name: "drag-ruler.genericSpeedProvider.speeds.walk"},
|
||||
{id: "dash", default: 0xffff00, name: "drag-ruler.genericSpeedProvider.speeds.dash"},
|
||||
];
|
||||
}
|
||||
|
||||
getRanges(token) {
|
||||
const speedAttribute = this.getSetting("speedAttribute");
|
||||
if (!speedAttribute) return [];
|
||||
const tokenSpeed = parseFloat(getProperty(token, speedAttribute));
|
||||
if (tokenSpeed === undefined) {
|
||||
console.warn(
|
||||
`Drag Ruler (Generic Speed Provider) | The configured token speed attribute "${speedAttribute}" didn't return a speed value. To use colors based on drag distance set the setting to the correct value (or clear the box to disable this feature).`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
const dashMultiplier = this.getSetting("dashMultiplier");
|
||||
if (!dashMultiplier) return [{range: tokenSpeed, color: "walk"}];
|
||||
return [
|
||||
{range: tokenSpeed, color: "walk"},
|
||||
{range: tokenSpeed * dashMultiplier, color: "dash"},
|
||||
];
|
||||
}
|
||||
|
||||
get settings() {
|
||||
return [
|
||||
{
|
||||
id: "speedAttribute",
|
||||
name: "drag-ruler.genericSpeedProvider.settings.speedAttribute.name",
|
||||
hint: "drag-ruler.genericSpeedProvider.settings.speedAttribute.hint",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: String,
|
||||
default: getDefaultSpeedAttribute(),
|
||||
},
|
||||
{
|
||||
id: "dashMultiplier",
|
||||
name: "drag-ruler.genericSpeedProvider.settings.dashMultiplier.name",
|
||||
hint: "drag-ruler.genericSpeedProvider.settings.dashMultiplier.hint",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: Number,
|
||||
default: getDefaultDashMultiplier(),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
export function getDefaultSpeedAttribute() {
|
||||
switch (game.system.id) {
|
||||
case "CoC7":
|
||||
return "actor.system.attribs.mov.value";
|
||||
case "dcc":
|
||||
return "actor.system.attributes.speed.value";
|
||||
case "dnd4e":
|
||||
return "actor.system.movement.walk.value";
|
||||
case "dnd5e":
|
||||
return "actor.system.attributes.movement.walk";
|
||||
case "lancer":
|
||||
return "actor.system.derived.speed";
|
||||
case "pf1":
|
||||
case "D35E":
|
||||
return "actor.system.attributes.speed.land.total";
|
||||
case "sfrpg":
|
||||
return "actor.system.attributes.speed.value";
|
||||
case "shadowrun5e":
|
||||
return "actor.system.movement.walk.value";
|
||||
case "swade":
|
||||
return "actor.system.stats.speed.adjusted";
|
||||
case "ds4":
|
||||
return "actor.system.combatValues.movement.total";
|
||||
case "splittermond":
|
||||
return "actor.derivedValues.speed.value";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function getDefaultDashMultiplier() {
|
||||
switch (game.system.id) {
|
||||
case "swade":
|
||||
return 0;
|
||||
case "dcc":
|
||||
case "dnd4e":
|
||||
case "dnd5e":
|
||||
case "lancer":
|
||||
case "pf1":
|
||||
case "D35E":
|
||||
case "sfrpg":
|
||||
case "shadowrun5e":
|
||||
case "ds4":
|
||||
return 2;
|
||||
case "CoC7":
|
||||
return 5;
|
||||
case "splittermond":
|
||||
return 3;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
import {getPixelsFromGridPosition} from "./foundry_fixes.js";
|
||||
import {findVertexSnapPoint} from "./hex_support.js";
|
||||
import {disableSnap, moveWithoutAnimation, togglePathfinding} from "./keybindings.js";
|
||||
import {settingsKey} from "./settings.js";
|
||||
|
||||
export function* zip(it1, it2) {
|
||||
for (let i = 0; i < Math.min(it1.length, it2.length); i++) {
|
||||
yield [it1[i], it2[i]];
|
||||
}
|
||||
}
|
||||
|
||||
export function* enumeratedZip(it1, it2) {
|
||||
let i = 0;
|
||||
for (const [v1, v2] of zip(it1, it2)) {
|
||||
yield [i, v1, v2];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
export function* iterPairs(l) {
|
||||
for (let i = 1; i < l.length; i++) {
|
||||
yield [l[i - 1], l[i]];
|
||||
}
|
||||
}
|
||||
|
||||
export function sum(arr) {
|
||||
return arr.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
||||
export function buildSnapPointTokenData(token) {
|
||||
const tokenData = {
|
||||
width: token.document.width,
|
||||
height: token.document.height,
|
||||
};
|
||||
|
||||
if (isModuleActive("hex-size-support")) {
|
||||
tokenData.hexSizeSupport = {};
|
||||
tokenData.hexSizeSupport.altSnappingFlag = CONFIG.hexSizeSupport.getAltSnappingFlag(token);
|
||||
tokenData.hexSizeSupport.altOrientationFlag =
|
||||
CONFIG.hexSizeSupport.getAltOrientationFlag(token);
|
||||
tokenData.hexSizeSupport.borderSize = token.document.getFlag("hex-size-support", "borderSize");
|
||||
}
|
||||
|
||||
return tokenData;
|
||||
}
|
||||
|
||||
export function getSnapPointForToken(x, y, token) {
|
||||
return getSnapPointForTokenData(x, y, buildSnapPointTokenData(token));
|
||||
}
|
||||
|
||||
export function getSnapPointForTokenDataObj(pos, tokenData) {
|
||||
return getSnapPointForTokenData(pos.x, pos.y, tokenData);
|
||||
}
|
||||
|
||||
function getSnapPointForTokenData(x, y, tokenData) {
|
||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||
return new PIXI.Point(x, y);
|
||||
}
|
||||
if (canvas.grid.isHex) {
|
||||
if (tokenData.hexSizeSupport?.altSnappingFlag) {
|
||||
if (tokenData.hexSizeSupport.borderSize % 2 === 0) {
|
||||
const snapPoint = findVertexSnapPoint(x, y, tokenData.hexSizeSupport.altOrientationFlag);
|
||||
return new PIXI.Point(snapPoint.x, snapPoint.y);
|
||||
} else {
|
||||
return new PIXI.Point(...canvas.grid.getCenter(x, y));
|
||||
}
|
||||
} else {
|
||||
return new PIXI.Point(...canvas.grid.getCenter(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
const [topLeftX, topLeftY] = canvas.grid.getTopLeft(x, y);
|
||||
let cellX, cellY;
|
||||
if (tokenData.width % 2 === 0) cellX = x - canvas.grid.h / 2;
|
||||
else cellX = x;
|
||||
if (tokenData.height % 2 === 0) cellY = y - canvas.grid.h / 2;
|
||||
else cellY = y;
|
||||
const [centerX, centerY] = canvas.grid.getCenter(cellX, cellY);
|
||||
let snapX, snapY;
|
||||
// Tiny tokens can snap to the cells corners
|
||||
if (tokenData.width <= 0.5) {
|
||||
const offsetX = x - topLeftX;
|
||||
const subGridWidth = Math.floor(canvas.grid.w / 2);
|
||||
const subGridPosX = Math.floor(offsetX / subGridWidth);
|
||||
snapX = topLeftX + (subGridPosX + 0.5) * subGridWidth;
|
||||
}
|
||||
// Tokens with odd multipliers (1x1, 3x3, ...) and tokens smaller than 1x1 but bigger than 0.5x0.5 snap to the center of the grid cell
|
||||
else if (Math.round(tokenData.width) % 2 === 1 || tokenData.width < 1) {
|
||||
snapX = centerX;
|
||||
}
|
||||
// All remaining tokens (those with even or fractional multipliers on square grids) snap to the intersection points of the grid
|
||||
else {
|
||||
snapX = centerX + canvas.grid.w / 2;
|
||||
}
|
||||
if (tokenData.height <= 0.5) {
|
||||
const offsetY = y - topLeftY;
|
||||
const subGridHeight = Math.floor(canvas.grid.h / 2);
|
||||
const subGridPosY = Math.floor(offsetY / subGridHeight);
|
||||
snapY = topLeftY + (subGridPosY + 0.5) * subGridHeight;
|
||||
} else if (Math.round(tokenData.height) % 2 === 1 || tokenData.height < 1) {
|
||||
snapY = centerY;
|
||||
} else {
|
||||
snapY = centerY + canvas.grid.h / 2;
|
||||
}
|
||||
return new PIXI.Point(snapX, snapY);
|
||||
}
|
||||
|
||||
export function getSnapPointForTokenObj(pos, token) {
|
||||
return getSnapPointForToken(pos.x, pos.y, token);
|
||||
}
|
||||
|
||||
export function getSnapPointForMeasuredTemplate(x, y) {
|
||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||
return new PIXI.Point(x, y);
|
||||
}
|
||||
return canvas.grid.grid.getSnappedPosition(x, y, canvas.templates.gridPrecision);
|
||||
}
|
||||
|
||||
export function getSnapPointForEntity(x, y, entity) {
|
||||
const isToken = entity instanceof Token;
|
||||
if (isToken) return getSnapPointForToken(x, y, entity);
|
||||
else return getSnapPointForMeasuredTemplate(x, y);
|
||||
}
|
||||
|
||||
export function highlightTokenShape(position, shape, color, alpha) {
|
||||
const layer = canvas.grid.highlightLayers[this.name];
|
||||
if (!layer) return false;
|
||||
const area = getAreaFromPositionAndShape(position, shape);
|
||||
for (const space of area) {
|
||||
const [x, y] = getPixelsFromGridPosition(space.x, space.y);
|
||||
canvas.grid.grid.highlightGridPosition(layer, {x, y, color, alpha: 0.25 * alpha});
|
||||
}
|
||||
}
|
||||
|
||||
export function getAreaFromPositionAndShape(position, shape) {
|
||||
return shape.map(space => {
|
||||
let x = position.x + space.x;
|
||||
let y = position.y + space.y;
|
||||
if (canvas.grid.isHex) {
|
||||
let shiftedRow;
|
||||
if (canvas.grid.grid.options.even) shiftedRow = 1;
|
||||
else shiftedRow = 0;
|
||||
if (canvas.grid.grid.options.columns) {
|
||||
if (space.x % 2 !== 0 && position.x % 2 !== shiftedRow) {
|
||||
y += 1;
|
||||
}
|
||||
} else {
|
||||
if (space.y % 2 !== 0 && position.y % 2 !== shiftedRow) {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {x, y};
|
||||
});
|
||||
}
|
||||
|
||||
export function getTokenShape(token) {
|
||||
return getTokenShapeForTokenData(buildSnapPointTokenData(token), token.scene);
|
||||
}
|
||||
|
||||
export function getTokenShapeForTokenData(tokenData, scene = canvas.scene) {
|
||||
if (scene.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||
return [{x: 0, y: 0}];
|
||||
} else if (scene.grid.type === CONST.GRID_TYPES.SQUARE) {
|
||||
const topOffset = -Math.floor(tokenData.height / 2);
|
||||
const leftOffset = -Math.floor(tokenData.width / 2);
|
||||
const shape = [];
|
||||
for (let y = 0; y < tokenData.height; y++) {
|
||||
for (let x = 0; x < tokenData.width; x++) {
|
||||
shape.push({x: x + leftOffset, y: y + topOffset});
|
||||
}
|
||||
}
|
||||
return shape;
|
||||
} else {
|
||||
// Hex grids
|
||||
if (game.modules.get("hex-size-support")?.active && tokenData.hexSizeSupport.altSnappingFlag) {
|
||||
const borderSize = tokenData.hexSizeSupport.borderSize;
|
||||
let shape = [{x: 0, y: 0}];
|
||||
if (borderSize >= 2)
|
||||
shape = shape.concat([
|
||||
{x: 0, y: -1},
|
||||
{x: -1, y: -1},
|
||||
]);
|
||||
if (borderSize >= 3)
|
||||
shape = shape.concat([
|
||||
{x: 0, y: 1},
|
||||
{x: -1, y: 1},
|
||||
{x: -1, y: 0},
|
||||
{x: 1, y: 0},
|
||||
]);
|
||||
if (borderSize >= 4)
|
||||
shape = shape.concat([
|
||||
{x: -2, y: -1},
|
||||
{x: 1, y: -1},
|
||||
{x: -1, y: -2},
|
||||
{x: 0, y: -2},
|
||||
{x: 1, y: -2},
|
||||
]);
|
||||
|
||||
if (Boolean(tokenData.hexSizeSupport.altOrientationFlag) !== canvas.grid.grid.options.columns)
|
||||
shape.forEach(space => (space.y *= -1));
|
||||
if (canvas.grid.grid.options.columns)
|
||||
shape = shape.map(space => {
|
||||
return {x: space.y, y: space.x};
|
||||
});
|
||||
return shape;
|
||||
} else {
|
||||
return [{x: 0, y: 0}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getTokenSize(token) {
|
||||
let w, h;
|
||||
const hexSizeSupportBorderSize = token.document.flags["hex-size-support"]?.borderSize;
|
||||
if (hexSizeSupportBorderSize > 0) {
|
||||
w = h = hexSizeSupportBorderSize;
|
||||
} else {
|
||||
w = token.document.width;
|
||||
h = token.document.height;
|
||||
}
|
||||
return {w, h};
|
||||
}
|
||||
|
||||
// Tokens that have a size divisible by two (2x2, 4x4, 2x1) have their ruler at the edge of a cell.
|
||||
// This function applies an offset to to the waypoints that will move the ruler from the edge to the center of the cell
|
||||
export function applyTokenSizeOffset(waypoints, token) {
|
||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||
return waypoints;
|
||||
}
|
||||
|
||||
const tokenSize = getTokenSize(token);
|
||||
const waypointOffset = {x: 0, y: 0};
|
||||
if (canvas.grid.isHex) {
|
||||
if (game.modules.get("hex-size-support")?.active) {
|
||||
const isAltOrientation = CONFIG.hexSizeSupport.getAltOrientationFlag(token);
|
||||
if (canvas.grid.grid.options.columns) {
|
||||
if (tokenSize.w % 2 === 0) {
|
||||
waypointOffset.x = canvas.grid.w / 2;
|
||||
if (!isAltOrientation) waypointOffset.x *= -1;
|
||||
}
|
||||
} else {
|
||||
if (tokenSize.h % 2 === 0) {
|
||||
waypointOffset.y = canvas.grid.h / 2;
|
||||
if (isAltOrientation) waypointOffset.y *= -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If hex size support isn't active leave the waypoints like they are
|
||||
} else {
|
||||
if (tokenSize.w % 2 === 0) {
|
||||
waypointOffset.x = canvas.grid.w / 2;
|
||||
}
|
||||
if (tokenSize.h % 2 === 0) {
|
||||
waypointOffset.y = canvas.grid.h / 2;
|
||||
}
|
||||
}
|
||||
|
||||
return waypoints.map(w => new PIXI.Point(w.x + waypointOffset.x, w.y + waypointOffset.y));
|
||||
}
|
||||
|
||||
export function setSnapParameterOnOptions(sourceObject, options) {
|
||||
// Allow outside modules to override snapping
|
||||
if (sourceObject.snapOverride?.active) {
|
||||
options.snapOverrideActive = true;
|
||||
options.snap = sourceObject.snapOverride.snap;
|
||||
sourceObject.snapOverride = undefined; // remove it to prevent any lingering data issues
|
||||
} else {
|
||||
options.snap = !disableSnap;
|
||||
}
|
||||
}
|
||||
|
||||
export function isClose(a, b, delta) {
|
||||
return Math.abs(a - b) <= delta;
|
||||
}
|
||||
|
||||
export function getMeasurePosition() {
|
||||
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(
|
||||
canvas.tokens,
|
||||
);
|
||||
const rulerOffset = canvas.controls.ruler.rulerOffset;
|
||||
const measurePosition = {x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y};
|
||||
return measurePosition;
|
||||
}
|
||||
|
||||
// isGM function for use during loading when game.user isn't available yet
|
||||
export function early_isGM() {
|
||||
const level = game.data.users.find(u => u._id == game.data.userId).role;
|
||||
const gmLevel = CONST.USER_ROLES.ASSISTANT;
|
||||
return level >= gmLevel;
|
||||
}
|
||||
|
||||
export function isModuleActive(moduleName) {
|
||||
return game.modules.get(moduleName)?.active;
|
||||
}
|
||||
|
||||
export function isPathfindingEnabled() {
|
||||
if (!window.routinglib) return false;
|
||||
if (this.user !== game.user) return false;
|
||||
if (!game.user.isGM && !game.settings.get(settingsKey, "allowPathfinding")) return false;
|
||||
if (moveWithoutAnimation) return false;
|
||||
return game.settings.get(settingsKey, "autoPathfinding") != togglePathfinding;
|
||||
}
|
||||
Reference in New Issue
Block a user