Reformat with prettier

This commit is contained in:
Manuel Vögele
2022-09-28 07:35:16 +02:00
parent 71424cd284
commit 5e82049686
19 changed files with 956 additions and 710 deletions
+4
View File
@@ -0,0 +1,4 @@
printWidth: 100
trailingComma: "all"
bracketSpacing: false
arrowParens: "avoid"
+101 -80
View File
@@ -1,105 +1,116 @@
import {measureDistances} from "./compatibility.js";
import {getMovementHistory} from "./movement_tracking.js";
import {GenericSpeedProvider, SpeedProvider} from "./speed_provider.js"
import {settingsKey} from "./settings.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
export const availableSpeedProviders = {};
export let currentSpeedProvider = undefined;
function register(module, type, speedProvider) {
const id = `${type}.${module.id}`
let providerInstance
const id = `${type}.${module.id}`;
let providerInstance;
if (speedProvider.prototype instanceof SpeedProvider) {
providerInstance = new speedProvider(id)
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;
}
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)
setupProvider(providerInstance);
}
function setupProvider(speedProvider) {
if (speedProvider instanceof SpeedProvider) {
const unreachableColor = {id: "unreachable", default: speedProvider.defaultUnreachableColor, name: "drag-ruler.settings.speedProviderSettings.color.unreachable.name"}
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)
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()
availableSpeedProviders[speedProvider.id] = speedProvider;
game.settings.settings.get("drag-ruler.speedProvider").default = getDefaultSpeedProvider();
updateSpeedProvider();
}
export function getDefaultSpeedProvider() {
const providerIds = Object.keys(availableSpeedProviders)
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
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
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]
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]
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)
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)
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}`)
range.color = game.settings.get(
settingsKey,
`speedProviders.${currentSpeedProvider.id}.color.${range.color}`,
);
}
return ranges
}
catch (e) {
console.error(e)
return []
return ranges;
} catch (e) {
console.error(e);
return [];
}
}
export function getUnreachableColorFromSpeedProvider() {
if (currentSpeedProvider instanceof Function)
return 0xFF0000
if (currentSpeedProvider instanceof Function) return 0xff0000;
try {
return game.settings.get(settingsKey, `speedProviders.${currentSpeedProvider.id}.color.unreachable`)
}
catch (e) {
console.error(e)
return 0xFF0000
return game.settings.get(
settingsKey,
`speedProviders.${currentSpeedProvider.id}.color.unreachable`,
);
} catch (e) {
console.error(e);
return 0xff0000;
}
}
@@ -109,77 +120,87 @@ export function getCostFromSpeedProvider(token, area, options) {
return SpeedProvider.prototype.getCostForStep.call(undefined, token, area, options);
}
return currentSpeedProvider.getCostForStep(token, area, options);
}
catch (e) {
} catch (e) {
console.error(e);
return 1;
}
}
export function getColorForDistanceAndToken(distance, token, ranges=null) {
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()});
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 = Ruler.dragRulerGetRaysFromWaypoints(history, {x: token.x, y: token.y}).map(ray => {return {ray}});
const segments = Ruler.dragRulerGetRaysFromWaypoints(history, {x: token.x, y: token.y}).map(
ray => {
return {ray};
},
);
const shape = getTokenShape(token);
const distances = measureDistances(segments, token, shape, {enableTerrainRuler: terrainRulerAvailable});
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);
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)
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
"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
"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)
register(module, "module", speedProvider);
}
export function registerSystem(systemId, speedProvider) {
const system = game.system
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
"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)
register(system, "system", speedProvider);
}
+47 -23
View File
@@ -3,19 +3,24 @@ 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}
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) {
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)
highlightTokenShape.call(this, space, tokenShape, color, alpha);
}
}
export function measureDistances(segments, entity, shape, options={}) {
const opts = duplicate(options)
export function measureDistances(segments, entity, shape, options = {}) {
const opts = duplicate(options);
if (canvas.grid.diagonalRule === "EUCL") {
opts.ignoreGrid = true;
opts.gridSpaes = false;
@@ -25,17 +30,23 @@ export function measureDistances(segments, entity, shape, options={}) {
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));
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;
opts.terrainRulerInitialState =
previousSegments[previousSegments.length - 1].ray.dragRulerFinalState;
return distances.concat(terrainRuler.measureDistances(newSegments, opts));
}
else {
} 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) {
if (!opts.ignoreGrid) {
opts.gridSpaces = true;
}
return canvas.grid.measureDistances(segments, opts);
@@ -44,23 +55,31 @@ export function measureDistances(segments, entity, shape, options={}) {
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.");
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>`,
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")
}
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")) {
} 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
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;
@@ -68,20 +87,25 @@ export function checkDependencies() {
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>`,
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()),
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())
close: () => game.settings.set(settingsKey, "lastTerrainRulerHintTime", Date.now()),
}).render(true);
}
}
+2 -2
View File
@@ -110,8 +110,8 @@ export class ProcessOnceQueue {
const newNode = {
value: element,
next: null,
previous: null
}
previous: null,
};
if (!this.first) {
this.first = newNode;
+5 -6
View File
@@ -3,18 +3,17 @@
// 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(yGrid, xGrid);
}
return canvas.grid.grid.getPixelsFromGridPosition(xGrid, yGrid)
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]
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) {
+128 -66
View File
@@ -1,12 +1,25 @@
import {highlightMeasurementTerrainRuler, measureDistances} from "./compatibility.js";
import {getGridPositionFromPixels, getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
import {
getGridPositionFromPixels,
getGridPositionFromPixelsObj,
getPixelsFromGridPositionObj,
} from "./foundry_fixes.js";
import {Line} from "./geometry.js";
import {disableSnap, moveWithoutAnimation} from "./keybindings.js";
import {trackRays} from "./movement_tracking.js"
import {trackRays} from "./movement_tracking.js";
import {findPath, isPathfindingEnabled} from "./pathfinding.js";
import {settingsKey} from "./settings.js";
import {recalculate} from "./socket.js";
import {applyTokenSizeOffset, enumeratedZip, getSnapPointForEntity, getSnapPointForToken, getSnapPointForTokenObj, getTokenShape, highlightTokenShape, sum} from "./util.js";
import {
applyTokenSizeOffset,
enumeratedZip,
getSnapPointForEntity,
getSnapPointForToken,
getSnapPointForTokenObj,
getTokenShape,
highlightTokenShape,
sum,
} from "./util.js";
// This is a modified version of Ruler.moveToken from foundry 0.7.9
export async function moveEntities(draggedEntity, selectedEntities) {
@@ -27,12 +40,14 @@ export async function moveEntities(draggedEntity, selectedEntities) {
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))
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();
@@ -46,8 +61,7 @@ export async function moveEntities(draggedEntity, selectedEntities) {
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();
if (this.draggedEntity?.id === draggedEntity.id) this._endMeasurement();
}
// This is a modified version code extracted from Ruler.moveToken from foundry 0.7.9
@@ -65,8 +79,7 @@ async function animateEntities(entities, draggedEntity, draggedRays, wasPaused)
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
dx = entity.data.x - origin[0];
dy = entity.data.y - origin[1];
}
else {
} else {
dx = entity.data.x - origin[0];
dy = entity.data.y - origin[1];
}
@@ -81,7 +94,7 @@ async function animateEntities(entities, draggedEntity, draggedRays, wasPaused)
// 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++) {
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];
@@ -92,18 +105,29 @@ async function animateEntities(entities, draggedEntity, draggedRays, wasPaused)
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});
await draggedEntity.scene.updateEmbeddedDocuments(
draggedEntity.constructor.embeddedName,
updates,
{animate},
);
if (animate)
await Promise.all(entityPaths.map(({entity}) => CanvasAnimation.getAnimation(entity.movementAnimationName)?.promise));
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));
entityAnimationData.forEach(ead => (ead.rays = ead.rays.slice(0, i + 1)));
break;
}
}
if (isToken)
trackRays(entities, entityAnimationData.map(({rays}) => rays)).then(() => recalculate(entities));
trackRays(
entities,
entityAnimationData.map(({rays}) => rays),
).then(() => recalculate(entities));
}
function calculateEntityOffset(entityA, entityB) {
@@ -111,7 +135,10 @@ function calculateEntityOffset(entityA, entityB) {
}
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});
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;
}
@@ -121,7 +148,10 @@ 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}
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();
@@ -140,12 +170,21 @@ function scheduleMeasurement(destination, event) {
event._measureTime = Date.now();
this._state = Ruler.STATES.MEASURING;
cancelScheduledMeasurement.call(this);
}
else {
} 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);
this.deferredMeasurementPromise = new Promise(
(resolve, reject) => (this.deferredMeasurementResolve = resolve),
);
this.deferredMeasurementTimeout = window.setTimeout(
() =>
scheduleMeasurement.call(
this,
this.deferredMeasurementData.destination,
this.deferredMeasurementData.event,
),
measurementInterval,
);
}
}
}
@@ -157,11 +196,9 @@ export function cancelScheduledMeasurement() {
}
// This is a modified version of Ruler.measure form foundry 0.7.9
export function measure(destination, options={}) {
export function measure(destination, options = {}) {
const isToken = this.draggedEntity instanceof Token;
if (isToken && !this.draggedEntity.isVisible)
return []
if (isToken && !this.draggedEntity.isVisible) return [];
options.snap = options.snap ?? !disableSnap;
@@ -175,32 +212,34 @@ export function measure(destination, options={}) {
const from = getGridPositionFromPixelsObj(this.waypoints[this.waypoints.length - 1]);
const to = getGridPositionFromPixelsObj(destination);
let path = findPath(from, to, this.draggedEntity, this.waypoints);
if (path)
path.shift();
if (path) path.shift();
if (path && path.length > 0) {
path = path.map(point => getSnapPointForTokenObj(getPixelsFromGridPositionObj(point), this.draggedEntity));
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)
if (
path[0].x === this.waypoints[this.waypoints.length - 1].x &&
path[0].y === this.waypoints[this.waypoints.length - 1].y
)
path = path.slice(1);
// If snapping is enabled, the last point of the path is already handled by the ruler
if (options.snap)
path = path.slice(0, path.length - 1);
if (options.snap) path = path.slice(0, path.length - 1);
for (const point of path) {
point.isPathfinding = true;
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
}
this.waypoints = this.waypoints.concat(path);
}
else {
} else {
// Don't show a path if the pathfinding yields no result to show the user that the destination is unreachable
destination = this.waypoints[this.waypoints.length - 1];
}
}
if(options.gridSpaces === undefined) {
if (options.gridSpaces === undefined) {
options.gridSpaces = canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS;
}
@@ -209,7 +248,7 @@ export function measure(destination, options={}) {
options.ignoreGrid = true;
}
if(options.ignoreGrid === undefined) {
if (options.ignoreGrid === undefined) {
options.ignoreGrid = false;
}
@@ -217,24 +256,26 @@ export function measure(destination, options={}) {
const waypoints = this.waypoints.concat([destination]);
// Move the waypoints to the center of the grid if a size is used that measures from edge to edge
const centeredWaypoints = isToken ? applyTokenSizeOffset(waypoints, this.draggedEntity) : duplicate(waypoints);
const centeredWaypoints = isToken
? applyTokenSizeOffset(waypoints, this.draggedEntity)
: duplicate(waypoints);
// Foundries native ruler requires the waypoints to sit in the dead center of the square to work properly
if (!options.enableTerrainRuler && !options.ignoreGrid)
centeredWaypoints.forEach(w => [w.x, w.y] = canvas.grid.getCenter(w.x, w.y));
centeredWaypoints.forEach(w => ([w.x, w.y] = canvas.grid.getCenter(w.x, w.y)));
const r = this.ruler;
this.destination = destination;
// Iterate over waypoints and construct segment rays
const segments = [];
const centeredSegments = []
const centeredSegments = [];
for (let [i, dest] of waypoints.slice(1).entries()) {
const centeredDest = centeredWaypoints[i + 1]
const centeredDest = centeredWaypoints[i + 1];
const origin = waypoints[i];
const centeredOrigin = centeredWaypoints[i]
const centeredOrigin = centeredWaypoints[i];
const label = this.labels.children[i];
const ray = new Ray(origin, dest);
const centeredRay = new Ray(centeredOrigin, centeredDest)
const centeredRay = new Ray(centeredOrigin, centeredDest);
ray.isPrevious = Boolean(origin.isPrevious);
centeredRay.isPrevious = ray.isPrevious;
ray.dragRulerVisitedSpaces = origin.dragRulerVisitedSpaces;
@@ -245,11 +286,10 @@ export function measure(destination, options={}) {
if (label) label.visible = false;
continue;
}
segments.push({ ray, label });
centeredSegments.push({ray: centeredRay, label})
segments.push({ray, label});
centeredSegments.push({ray: centeredRay, label});
}
const shape = isToken ? getTokenShape(this.draggedEntity) : null;
// Compute measured distance
@@ -258,9 +298,9 @@ export function measure(destination, options={}) {
let totalDistance = 0;
for (let [i, d] of distances.entries()) {
let s = centeredSegments[i];
s.startDistance = totalDistance
s.startDistance = totalDistance;
totalDistance += d;
s.last = i === (centeredSegments.length - 1);
s.last = i === centeredSegments.length - 1;
s.distance = d;
s.text = this._getSegmentLabel(d, totalDistance, s.last);
}
@@ -271,18 +311,24 @@ export function measure(destination, options={}) {
// Draw measured path
r.clear();
let rulerColor
let rulerColor;
if (!options.gridSpaces || canvas.grid.type === CONST.GRID_TYPES.GRIDLESS)
rulerColor = this.dragRulerGetColorForDistance(totalDistance);
else
rulerColor = this.color
for (const [i, s, cs] of enumeratedZip([...segments].reverse(), [...centeredSegments].reverse())) {
const { label, text, last } = cs;
else rulerColor = this.color;
for (const [i, s, cs] of enumeratedZip(
[...segments].reverse(),
[...centeredSegments].reverse(),
)) {
const {label, text, last} = cs;
// Draw line segment
const opacityMultiplier = s.ray.isPrevious ? 0.33 : 1;
r.lineStyle(6, 0x000000, 0.5 * opacityMultiplier).moveTo(s.ray.A.x, s.ray.A.y).lineTo(s.ray.B.x, s.ray.B.y)
.lineStyle(4, rulerColor, 0.25 * opacityMultiplier).moveTo(s.ray.A.x, s.ray.A.y).lineTo(s.ray.B.x, s.ray.B.y);
r.lineStyle(6, 0x000000, 0.5 * opacityMultiplier)
.moveTo(s.ray.A.x, s.ray.A.y)
.lineTo(s.ray.B.x, s.ray.B.y)
.lineStyle(4, rulerColor, 0.25 * opacityMultiplier)
.moveTo(s.ray.A.x, s.ray.A.y)
.lineTo(s.ray.B.x, s.ray.B.y);
// Draw the distance label just after the endpoint of the segment
if (label) {
@@ -296,11 +342,14 @@ export function measure(destination, options={}) {
const rayLabelXHitY = rayLine.calcY(labelPosition.x);
let innerDistance;
// If ray hits top or bottom side of label
if (rayLine.isVertical || rayLabelXHitY < labelPosition.y || rayLabelXHitY > labelPosition.y + label.height)
innerDistance = Math.abs((label.height / 2) / Math.sin(s.ray.angle));
if (
rayLine.isVertical ||
rayLabelXHitY < labelPosition.y ||
rayLabelXHitY > labelPosition.y + label.height
)
innerDistance = Math.abs(label.height / 2 / Math.sin(s.ray.angle));
// If ray hits left or right side of label
else
innerDistance = Math.abs((label.width / 2) / Math.cos(s.ray.angle));
else innerDistance = Math.abs(label.width / 2 / Math.cos(s.ray.angle));
labelPosition = s.ray.project((s.ray.distance + 50 + innerDistance) / s.ray.distance);
labelPosition.x -= label.width / 2;
labelPosition.y -= label.height / 2;
@@ -310,9 +359,14 @@ export function measure(destination, options={}) {
// Highlight grid positions
if (isToken && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS && options.gridSpaces) {
if (options.enableTerrainRuler) {
highlightMeasurementTerrainRuler.call(this, cs.ray, cs.startDistance, shape, opacityMultiplier)
}
else {
highlightMeasurementTerrainRuler.call(
this,
cs.ray,
cs.startDistance,
shape,
opacityMultiplier,
);
} else {
const previousSegments = centeredSegments.slice(0, segments.length - 1 - i);
highlightMeasurementNative.call(this, cs.ray, previousSegments, shape, opacityMultiplier);
}
@@ -328,22 +382,30 @@ export function measure(destination, options={}) {
return segments;
}
export function highlightMeasurementNative(ray, previousSegments, tokenShape=[{x: 0, y: 0}], alpha=1) {
export function highlightMeasurementNative(
ray,
previousSegments,
tokenShape = [{x: 0, y: 0}],
alpha = 1,
) {
const spacer = canvas.scene.data.gridType === CONST.GRID_TYPES.SQUARE ? 1.41 : 1;
const nMax = Math.max(Math.floor(ray.distance / (spacer * Math.min(canvas.grid.w, canvas.grid.h))), 1);
const tMax = Array.fromRange(nMax+1).map(t => t / nMax);
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() ) {
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 [x0, y0] = i === 0 ? [null, null] : prior;
let [x1, y1] = canvas.grid.grid.getGridPositionFromPixels(x, y);
if ( x0 === x1 && y0 === y1 ) continue;
if (x0 === x1 && y0 === y1) continue;
// Highlight the grid position
let [xgtl, ygtl] = canvas.grid.grid.getPixelsFromGridPosition(x1, y1);
@@ -358,7 +420,7 @@ export function highlightMeasurementNative(ray, previousSegments, tokenShape=[{x
// 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 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);
+32 -35
View File
@@ -18,76 +18,73 @@ export function findVertexSnapPoint(x, y, altOrientationFlag) {
}
function findSnapPointRows(x, y, h, w, alt) {
let xOffset = 0.0
let xOffset = 0.0;
if (canvas.grid.grid.even) {
xOffset = -0.5
xOffset = -0.5;
}
let yOffset1 = 0.75
let yOffset2 = 0.00
let yOffset1 = 0.75;
let yOffset2 = 0.0;
if (alt) {
yOffset1 = 0.25
yOffset2 = 1.00
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)
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
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 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)
let snapX = c * w - (1 - xOff) * w;
let snapY = r * h * 1.5 - (1.5 - yOff) * h;
return {x: snapX, y: snapY}
return {x: snapX, y: snapY};
}
function findSnapPointCols(x, y, h, w, alt) {
let yOffset = 0.0
let yOffset = 0.0;
if (canvas.grid.grid.even) {
yOffset = -0.5
yOffset = -0.5;
}
let xOffset1 = 0.25
let xOffset2 = 1.00
let xOffset1 = 0.25;
let xOffset2 = 1.0;
if (alt) {
xOffset1 = 0.75
xOffset2 = 0.00
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)
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
return row1;
} else {
return row2;
}
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 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)
let snapX = c * w * 1.5 - (1.5 - xOff) * w;
let snapY = r * h - (1 - yOff) * h;
return {x: snapX, y: snapY}
return {x: snapX, y: snapY};
}
+30 -32
View File
@@ -9,18 +9,22 @@ export function registerKeybindings() {
game.keybindings.register(settingsKey, "cancelDrag", {
name: "drag-ruler.keybindings.cancelDrag",
onDown: cancelDrag,
uneditable: [{
key: "Escape",
}],
uneditable: [
{
key: "Escape",
},
],
precedence: -1,
});
game.keybindings.register(settingsKey, "createWaypoint", {
name: "drag-ruler.keybindings.createWaypoint",
onDown: handleCreateWaypoint,
editable: [{
key: "Space"
}],
editable: [
{
key: "Space",
},
],
precedence: -1,
});
@@ -35,9 +39,11 @@ export function registerKeybindings() {
hint: "drag-ruler.keybindings.disableSnap.hint",
onDown: handleDisableSnap,
onUp: handleDisableSnap,
editable: [{
key: "ShiftLeft",
}],
editable: [
{
key: "ShiftLeft",
},
],
precedence: -1,
});
@@ -46,9 +52,11 @@ export function registerKeybindings() {
hint: "drag-ruler.keybindings.moveWithoutAnimation.hint",
onDown: handleMoveWithoutAnimation,
onUp: handleMoveWithoutAnimation,
editable: [{
key: "AltLeft",
}],
editable: [
{
key: "AltLeft",
},
],
precedence: -1,
});
@@ -64,8 +72,7 @@ export function registerKeybindings() {
function handleDeleteWaypoint() {
const ruler = canvas.controls.ruler;
if (!ruler?.draggedEntity)
return false;
if (!ruler?.draggedEntity) return false;
ruler.dragRulerDeleteWaypoint();
return true;
}
@@ -74,16 +81,14 @@ 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;
if (!ruler?.draggedEntity) return false;
let options = {};
setSnapParameterOnOptions(ruler, options);
if (ruler._state === Ruler.STATES.INACTIVE) {
ruler.dragRulerStart(options);
}
else {
} else {
ruler.dragRulerAddWaypoint(getMeasurePosition(), options);
}
return true;
@@ -91,8 +96,7 @@ function handleCreateWaypoint() {
function cancelDrag() {
const ruler = canvas.controls.ruler;
if (!ruler?.draggedEntity)
return false;
if (!ruler?.draggedEntity) return false;
ruler.dragRulerAbortDrag();
return true;
}
@@ -101,10 +105,8 @@ function handleDisableSnap(event) {
disableSnap = !event.up;
const ruler = canvas.controls.ruler;
if (!ruler?.isDragRuler)
return false;
if (ruler._state !== Ruler.STATES.MEASURING)
return false;
if (!ruler?.isDragRuler) return false;
if (ruler._state !== Ruler.STATES.MEASURING) return false;
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
ruler.dragRulerSendState();
@@ -115,10 +117,8 @@ function handleMoveWithoutAnimation(event) {
moveWithoutAnimation = !event.up;
const ruler = canvas.controls.ruler;
if (!ruler?.isDragRuler)
return false;
if (ruler._state !== Ruler.STATES.MEASURING)
return false;
if (!ruler?.isDragRuler) return false;
if (ruler._state !== Ruler.STATES.MEASURING) return false;
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
ruler.dragRulerSendState();
@@ -129,10 +129,8 @@ function handleTogglePathfinding(event) {
togglePathfinding = !event.up;
const ruler = canvas.controls.ruler;
if (!ruler?.isDragRuler)
return false;
if (ruler._state !== Ruler.STATES.MEASURING)
return false;
if (!ruler?.isDragRuler) return false;
if (ruler._state !== Ruler.STATES.MEASURING) return false;
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
ruler.dragRulerSendState();
+48 -31
View File
@@ -1,73 +1,90 @@
// SPDX-License-Identifier: MIT
// Copyright © 2021 fvtt-lib-wrapper Rui Pinheiro
'use strict';
"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');
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', () => {
Hooks.once("init", () => {
// Check if the real module is already loaded - if so, use it
if(globalThis.libWrapper && !(globalThis.libWrapper.is_fallback ?? true)) {
if (globalThis.libWrapper && !(globalThis.libWrapper.is_fallback ?? true)) {
libWrapper = globalThis.libWrapper;
return;
}
// Fallback implementation
libWrapper = class {
static get is_fallback() { return true };
static get is_fallback() {
return true;
}
static get WRAPPER() { return 'WRAPPER' };
static get MIXED() { return 'MIXED' };
static get OVERRIDE() { return 'OVERRIDE' };
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');
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];
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) {
if (split.length == 0) {
obj = globalThis;
fn_name = root_nm;
}
else {
} else {
const _eval = eval;
fn_name = split.pop();
obj = split.reduce((x,y)=>x[y], globalThis[root_nm] ?? _eval(root_nm));
obj = split.reduce((x, y) => x[y], globalThis[root_nm] ?? _eval(root_nm));
}
let iObj = obj;
let descriptor = null;
while(iObj) {
while (iObj) {
descriptor = Object.getOwnPropertyDescriptor(iObj, fn_name);
if(descriptor) break;
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.`;
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); };
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) {
if (!is_setter) {
if (descriptor.value) {
original = descriptor.value;
descriptor.value = wrapper;
}
else {
} else {
original = descriptor.get;
descriptor.get = wrapper;
}
}
else {
if(!descriptor.set) throw `libWrapper Shim: '${target}' does not have a setter`;
} else {
if (!descriptor.set) throw `libWrapper Shim: '${target}' does not have a setter`;
original = descriptor.set;
descriptor.set = wrapper;
}
@@ -75,5 +92,5 @@ Hooks.once('init', () => {
descriptor.configurable = true;
Object.defineProperty(obj, fn_name, descriptor);
}
}
};
});
+115 -74
View File
@@ -1,20 +1,31 @@
"use strict"
"use strict";
import {getColorForDistanceAndToken, getMovedDistanceFromToken, getRangesFromSpeedProvider, initApi, registerModule, registerSystem} from "./api.js";
import {
getColorForDistanceAndToken,
getMovedDistanceFromToken,
getRangesFromSpeedProvider,
initApi,
registerModule,
registerSystem,
} from "./api.js";
import {checkDependencies, getHexSizeSupportTokenGridCenter} from "./compatibility.js";
import {moveEntities, onMouseMove} from "./foundry_imports.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 {performMigrations} from "./migration.js";
import {removeLastHistoryEntryIfAt, resetMovementHistory} from "./movement_tracking.js";
import {wipePathfindingCache, initializePathfinding, startBackgroundCaching} from "./pathfinding.js";
import {
wipePathfindingCache,
initializePathfinding,
startBackgroundCaching,
} from "./pathfinding.js";
import {extendRuler} from "./ruler.js";
import {registerSettings, RightClickAction, settingsKey} from "./settings.js"
import {registerSettings, RightClickAction, settingsKey} from "./settings.js";
import {recalculate} from "./socket.js";
import {SpeedProvider} from "./speed_provider.js"
import {SpeedProvider} from "./speed_provider.js";
import {setSnapParameterOnOptions} from "./util.js";
import initGridlessPathfinding, * as GridlessPathfinding from "../wasm/gridless_pathfinding.js"
import initGridlessPathfinding, * as GridlessPathfinding from "../wasm/gridless_pathfinding.js";
CONFIG.debug.dragRuler = false;
export let debugGraphics = undefined;
@@ -37,7 +48,7 @@ initGridlessPathfinding().then(() => {
});
// Whenever a token the current user controls updates, start caching
Hooks.on("updateToken", (document) => {
Hooks.on("updateToken", document => {
const token = document.object;
if (token._controlled) {
startBackgroundCaching(token);
@@ -46,12 +57,17 @@ initGridlessPathfinding().then(() => {
});
Hooks.once("init", () => {
registerSettings()
registerKeybindings()
initApi()
registerSettings();
registerKeybindings();
initApi();
hookDragHandlers(Token);
hookDragHandlers(MeasuredTemplate);
libWrapper.register("drag-ruler", "TokenLayer.prototype.undoHistory", tokenLayerUndoHistory, "WRAPPER");
libWrapper.register(
"drag-ruler",
"TokenLayer.prototype.undoHistory",
tokenLayerUndoHistory,
"WRAPPER",
);
extendRuler();
@@ -62,16 +78,15 @@ Hooks.once("init", () => {
registerSystem,
recalculate,
resetMovementHistory,
}
})
};
});
Hooks.once("ready", () => {
performMigrations()
performMigrations();
checkDependencies();
Hooks.callAll("dragRuler.ready", SpeedProvider)
if (CONFIG.debug.dragRuler)
debugGraphics = canvas.controls.addChild(new PIXI.Container());
})
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 => {
@@ -79,37 +94,61 @@ Hooks.on("canvasReady", () => {
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')),
callback: li => resetMovementHistory(ui.combat.viewed, li.data("combatant-id")),
};
menu.splice(1, 0, entry);
});
function forwardIfUnahndled(newFn) {
return function(oldFn, ...args) {
return function (oldFn, ...args) {
const eventHandled = newFn(...args);
if (!eventHandled)
oldFn(...args);
if (!eventHandled) oldFn(...args);
};
}
function hookDragHandlers(entityType) {
const entityName = entityType.name
libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftStart`, onEntityLeftDragStart, "WRAPPER");
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");
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");
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) {
@@ -127,14 +166,21 @@ async function tokenLayerUndoHistory(wrapped) {
function onEntityLeftDragStart(wrapped, event) {
wrapped(event);
const isToken = this instanceof Token;
const ruler = canvas.controls.ruler
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))
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};
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);
@@ -149,48 +195,42 @@ function onEntityLeftDragMoveSnap(wrapped, event) {
function onEntityLeftDragMove(wrapped, event) {
wrapped(event);
const ruler = canvas.controls.ruler
if (ruler.isDragRuler)
onMouseMove.call(ruler, event)
const ruler = canvas.controls.ruler;
if (ruler.isDragRuler) onMouseMove.call(ruler, event);
}
function onEntityDragLeftDrop(event) {
const ruler = canvas.controls.ruler
const ruler = canvas.controls.ruler;
if (!ruler.isDragRuler) {
ruler.draggedEntity = undefined;
return false
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
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);
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
if (ruler._state === Ruler.STATES.STARTING) onMouseMove.call(ruler, event);
ruler._state = Ruler.STATES.MOVING;
moveEntities.call(ruler, ruler.draggedEntity, selectedTokens);
return true
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 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;
if (rightClickAction !== RightClickAction.CREATE_WAYPOINT) return false;
ruler.dragRulerStart(options);
event.preventDefault();
}
else if (ruler._state === Ruler.STATES.MEASURING) {
} else if (ruler._state === Ruler.STATES.MEASURING) {
switch (rightClickAction) {
case RightClickAction.CREATE_WAYPOINT:
event.preventDefault();
@@ -209,14 +249,10 @@ function onEntityDragLeftCancel(event) {
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;
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;
@@ -226,7 +262,9 @@ function applyGridlessSnapping(event) {
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active;
if (terrainRulerAvailable) {
const segments = Ruler.dragRulerGetRaysFromWaypoints(ruler.waypoints, destination).map(ray => {return {ray}});
const segments = Ruler.dragRulerGetRaysFromWaypoints(ruler.waypoints, destination).map(ray => {
return {ray};
});
const pinpointDistances = new Map();
for (const range of ranges) {
pinpointDistances.set(range.range, null);
@@ -234,7 +272,7 @@ function applyGridlessSnapping(event) {
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]);
.reduce((value, current) => (value[0] > current[0] ? value : current), [0, null]);
const rasterLocation = targetDistance[1];
if (rasterLocation) {
const deltaX = destination.x - rasterLocation.x;
@@ -245,12 +283,15 @@ function applyGridlessSnapping(event) {
event.data.destination.y = rasterLocation.y - ruler.rulerOffset.y;
}
}
}
else {
} else {
let waypointDistance = 0;
let origin = event.data.origin;
if (ruler.waypoints.length > 1) {
const segments = Ruler.dragRulerGetRaysFromWaypoints(ruler.waypoints, destination).map(ray => {return {ray}});
const segments = Ruler.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};
@@ -263,13 +304,13 @@ function applyGridlessSnapping(event) {
let targetDistance = ranges
.map(range => range.range)
.map(range => range - waypointDistance)
.map(range => range * canvas.dimensions.size / canvas.dimensions.distance)
.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;
event.data.destination.x = origin.x + (deltaX * targetDistance) / distance;
event.data.destination.y = origin.y + (deltaY * targetDistance) / distance;
}
}
}
+12 -12
View File
@@ -1,26 +1,24 @@
import {RightClickAction, settingsKey} from "./settings.js"
import {RightClickAction, settingsKey} from "./settings.js";
const currentDataVersion = "1.10.0"
const currentDataVersion = "1.10.0";
export async function performMigrations() {
if (game.user.isGM)
await performWorldMigraionts()
await performClientMigrations()
if (game.user.isGM) await performWorldMigraionts();
await performClientMigrations();
}
async function performWorldMigraionts() {
let dataVersion = game.settings.get(settingsKey, "dataVersion");
if (dataVersion === currentDataVersion)
return;
if (dataVersion === currentDataVersion) return;
if (dataVersion === "fresh install") {
game.settings.set(settingsKey, "dataVersion", currentDataVersion)
return
game.settings.set(settingsKey, "dataVersion", currentDataVersion);
return;
}
if (dataVersion === "1.3.0") {
dataVersion = "1.10.0"
dataVersion = "1.10.0";
}
game.settings.set(settingsKey, "dataVersion", dataVersion);
@@ -31,13 +29,15 @@ async function performClientMigrations() {
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`);
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)
game.settings.set(settingsKey, "clientDataVersion", currentDataVersion);
}
}
+21 -25
View File
@@ -9,38 +9,35 @@ function initTrackingFlag(combatant) {
if (isNaN(dragRulerFlag.trackedRound)) {
mergeObject(dragRulerFlag, initialFlag);
}
}
else {
} else {
combatant.data.flags.dragRuler = initialFlag;
}
}
function getInitializedCombatant(token, combat) {
const combatant = combat.getCombatantByToken(token.id);
if (!combatant)
return undefined;
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 (!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);
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;
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) {
@@ -56,7 +53,10 @@ function calculateUpdate(combat, token, 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});
measureDistances([{ray}], token, getTokenShape(token), {
terrainRulerInitialState: waypoints[waypoints.length - 1]?.dragRulerFinalState,
enableTerrainRuler: terrainRulerAvailable,
});
ray.A.dragRulerVisitedSpaces = ray.terrainRulerVisitedSpaces;
ray.A.dragRulerFinalState = ray.terrainRulerFinalState;
}
@@ -68,23 +68,18 @@ function calculateUpdate(combat, token, rays) {
export function getMovementHistory(token) {
const combat = game.combat;
if (!combat)
return [];
if (!combat) return [];
const combatant = combat.getCombatantByToken(token.id);
if (!combatant)
return [];
if (!combatant) return [];
const dragRulerFlags = combatant.data.flags.dragRuler;
if (!dragRulerFlags)
return [];
if (combat.data.round > dragRulerFlags.trackedRound)
return [];
if (!dragRulerFlags) return [];
if (combat.data.round > dragRulerFlags.trackedRound) return [];
return dragRulerFlags.passedWaypoints ?? [];
}
export async function removeLastHistoryEntryIfAt(token, x, y) {
const history = getMovementHistory(token);
if (history.length === 0)
return;
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;
@@ -92,14 +87,15 @@ export async function removeLastHistoryEntryIfAt(token, x, y) {
history.pop();
const combat = game.combat;
const combatant = combat.getCombatantByToken(token.id);
await updateCombatantDragRulerFlags(combat, [{_id: combatant.id, dragRulerFlags: combatant.data.flags.dragRuler}]);
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;
if (!dragRulerFlags) return;
dragRulerFlags.passedWaypoints = null;
dragRulerFlags.trackedRound = null;
dragRulerFlags.rulerState = null;
+99 -65
View File
@@ -1,8 +1,19 @@
import {getCenterFromGridPositionObj, getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
import {
getCenterFromGridPositionObj,
getGridPositionFromPixelsObj,
getPixelsFromGridPositionObj,
} from "./foundry_fixes.js";
import {moveWithoutAnimation, togglePathfinding} from "./keybindings.js";
import {debugGraphics} from "./main.js";
import {settingsKey} from "./settings.js";
import {buildSnapPointTokenData, getSnapPointForTokenDataObj, getTokenShape, getTokenShapeForTokenData, isModuleActive, iterPairs} from "./util.js";
import {
buildSnapPointTokenData,
getSnapPointForTokenDataObj,
getTokenShape,
getTokenShapeForTokenData,
isModuleActive,
iterPairs,
} from "./util.js";
import * as GridlessPathfinding from "../wasm/gridless_pathfinding.js";
import {PriorityQueueSet, ProcessOnceQueue} from "./data_structures.js";
@@ -60,8 +71,8 @@ class Cache {
this.background = {
nextJobId: null,
nextTimeoutId: null,
nextAnimationFrameId: null
}
nextAnimationFrameId: null,
};
}
clear() {
@@ -98,8 +109,10 @@ class Cache {
// Check if we already have the max number of layers. If we do,
// get rid of the one that hasn't been used for the longest
if (this.layers.size >= Cache.maxCacheLayers) {
const oldestCache = Array.from(this.layers.values())
.reduce((layer1, layer2) => (layer1?.lastUsed < layer2.lastUsed) ? layer1 : layer2, null);
const oldestCache = Array.from(this.layers.values()).reduce(
(layer1, layer2) => (layer1?.lastUsed < layer2.lastUsed ? layer1 : layer2),
null,
);
this.layers.delete(oldestCache.cacheId);
}
@@ -119,7 +132,7 @@ class Cache {
*/
startBackgroundCaching(token) {
const cacheLayer = this.getCacheLayer(token);
const tokenPosition = getGridPositionFromPixelsObj(token.position)
const tokenPosition = getGridPositionFromPixelsObj(token.position);
cacheLayer.queue.push(cacheLayer.nodes[tokenPosition.y][tokenPosition.x]);
@@ -137,8 +150,8 @@ class Cache {
// Find the latest-used cache that has nodes left to cache
const latestCache = this.getLatestCacheWithNonEmptyQueue();
if (latestCache) {
this.background.nextJobId = window.requestIdleCallback(
() => this.runBackgroundCache(latestCache)
this.background.nextJobId = window.requestIdleCallback(() =>
this.runBackgroundCache(latestCache),
);
this.resetAnimationFrameTimeout();
}
@@ -152,13 +165,10 @@ class Cache {
this.cancelTimeout();
this.cancelAnimationFrame();
this.background.nextTimeoutId = window.setTimeout(
() => {
this.scheduleAnimationFrameCache();
this.background.nextTimeoutId = null;
},
Cache.backgroundCachingTimeoutMillis
);
this.background.nextTimeoutId = window.setTimeout(() => {
this.scheduleAnimationFrameCache();
this.background.nextTimeoutId = null;
}, Cache.backgroundCachingTimeoutMillis);
}
/**
@@ -167,8 +177,8 @@ class Cache {
scheduleAnimationFrameCache() {
const latestCache = this.getLatestCacheWithNonEmptyQueue();
if (latestCache) {
this.background.nextAnimationFrameId = window.requestAnimationFrame(
() => this.runAnimationCache(latestCache)
this.background.nextAnimationFrameId = window.requestAnimationFrame(() =>
this.runAnimationCache(latestCache),
);
}
}
@@ -179,7 +189,7 @@ class Cache {
getLatestCacheWithNonEmptyQueue() {
return Array.from(this.layers.values())
.filter(layer => layer.queue.hasNext())
.reduce((layer1, layer2) => (layer1?.lastUsed > layer2.lastUsed) ? layer1 : layer2, null);
.reduce((layer1, layer2) => (layer1?.lastUsed > layer2.lastUsed ? layer1 : layer2), null);
}
/**
@@ -238,12 +248,9 @@ let gridlessPathfinders = new Map();
let gridWidth, gridHeight;
export function isPathfindingEnabled() {
if (this.user !== game.user)
return false;
if (!game.user.isGM && !game.settings.get(settingsKey, "allowPathfinding"))
return false;
if (moveWithoutAnimation)
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;
}
@@ -253,7 +260,12 @@ export function findPath(from, to, token, previousWaypoints) {
let pathfinder = gridlessPathfinders.get(tokenSize);
if (!pathfinder) {
let radiusMultiplier = game.settings.get(settingsKey, "pathfindingRadius");
pathfinder = GridlessPathfinding.initialize(canvas.walls.placeables, tokenSize * radiusMultiplier, token.data.elevation, Boolean(game.modules.get("wall-height")?.active));
pathfinder = GridlessPathfinding.initialize(
canvas.walls.placeables,
tokenSize * radiusMultiplier,
token.data.elevation,
Boolean(game.modules.get("wall-height")?.active),
);
gridlessPathfinders.set(tokenSize, pathfinder);
}
paintGridlessPathfindingDebug(pathfinder);
@@ -261,30 +273,36 @@ export function findPath(from, to, token, previousWaypoints) {
} else {
const cacheLayer = cache.getCacheLayer(token);
const firstNode = calculatePath(from, to, cacheLayer, previousWaypoints);
if (!firstNode)
return null;
if (!firstNode) return null;
paintGriddedPathfindingDebug(firstNode, cacheLayer.tokenData);
const path = [];
let currentNode = firstNode;
while (currentNode) {
if (path.length >= 2 && !stepCollidesWithWall(path[path.length - 2], currentNode.node, cacheLayer.tokenData)) {
if (
path.length >= 2 &&
!stepCollidesWithWall(path[path.length - 2], currentNode.node, cacheLayer.tokenData)
) {
// Replace last waypoint if the current waypoint leads to a valid path that isn't longer than the old path
if (window.terrainRuler) {
let startNode = getCenterFromGridPositionObj(path[path.length - 2]);
let middleNode = getCenterFromGridPositionObj(path[path.length - 1]);
let endNode = getCenterFromGridPositionObj(currentNode.node);
let oldPath = [{ray: new Ray(startNode, middleNode)}, {ray: new Ray(middleNode, endNode)}];
let oldPath = [
{ray: new Ray(startNode, middleNode)},
{ray: new Ray(middleNode, endNode)},
];
let newPath = [{ray: new Ray(startNode, endNode)}];
let costFunction = buildCostFunction(token, getTokenShape(token));
// TODO Cache the used measurement for use in the next loop to improve performance
let oldDistance = terrainRuler.measureDistances(oldPath, {costFunction}).reduce((a, b) => a + b);
let oldDistance = terrainRuler
.measureDistances(oldPath, {costFunction})
.reduce((a, b) => a + b);
let newDistance = terrainRuler.measureDistances(newPath, {costFunction})[0];
// TODO We might need to check if the diagonal count has increased on 5-10-5
if (newDistance < oldDistance) {
if (newDistance < oldDistance) {
path.pop();
}
else if (newDistance === oldDistance) {
} else if (newDistance === oldDistance) {
let oldNoDiagonals = oldPath[1].ray.terrainRulerFinalState?.noDiagonals;
let newNoDiagonals = newPath[0].ray.terrainRulerFinalState?.noDiagonals;
// This uses === && < instead of <= because the variables might be undefined (which shall lead to a true result)
@@ -292,8 +310,7 @@ export function findPath(from, to, token, previousWaypoints) {
path.pop();
}
}
}
else {
} else {
path.pop();
}
}
@@ -323,18 +340,36 @@ function getNode(pos, cacheLayer, initialize = true) {
const node = cacheLayer.nodes[pos.y][pos.x];
if (initialize && !node.edges) {
node.edges = [];
for (const neighborPos of canvas.grid.grid.getNeighbors(pos.y, pos.x).map(([y, x]) => {return {x, y};})) {
if (neighborPos.x < 0 || neighborPos.y < 0 || neighborPos.x >= gridWidth || neighborPos.y >= gridHeight) {
for (const neighborPos of canvas.grid.grid.getNeighbors(pos.y, pos.x).map(([y, x]) => {
return {x, y};
})) {
if (
neighborPos.x < 0 ||
neighborPos.y < 0 ||
neighborPos.x >= gridWidth ||
neighborPos.y >= gridHeight
) {
continue;
}
// TODO Work with pixels instead of grid locations
if (!stepCollidesWithWall(pos, neighborPos, cacheLayer.tokenData)) {
const isDiagonal = node.x !== neighborPos.x && node.y !== neighborPos.y && canvas.grid.type === CONST.GRID_TYPES.SQUARE;
const isDiagonal =
node.x !== neighborPos.x &&
node.y !== neighborPos.y &&
canvas.grid.type === CONST.GRID_TYPES.SQUARE;
let edgeCost;
if (window.terrainRuler) {
let ray = new Ray(getCenterFromGridPositionObj(pos), getCenterFromGridPositionObj(neighborPos));
let measuredDistance = terrainRuler.measureDistances([{ray}], {costFunction: buildCostFunction(cacheLayer.tokenData, getTokenShapeForTokenData(cacheLayer.tokenData))})[0];
let ray = new Ray(
getCenterFromGridPositionObj(pos),
getCenterFromGridPositionObj(neighborPos),
);
let measuredDistance = terrainRuler.measureDistances([{ray}], {
costFunction: buildCostFunction(
cacheLayer.tokenData,
getTokenShapeForTokenData(cacheLayer.tokenData),
),
})[0];
edgeCost = Math.round(measuredDistance / canvas.dimensions.distance);
if (ray.terrainRulerFinalState?.noDiagonals === 1) {
edgeCost = 1.5;
@@ -343,8 +378,7 @@ function getNode(pos, cacheLayer, initialize = true) {
if (isDiagonal && edgeCost == 1) {
edgeCost = 1.0001;
}
}
else {
} else {
// Count 5-10-5 diagonals as 1.5 (so two add up to 3) and 5-5-5 diagonals as 1.0001 (to discourage unnecessary diagonals)
// TODO Account for difficult terrain
edgeCost = isDiagonal ? (use5105 ? 1.5 : 1.0001) : 1;
@@ -365,17 +399,18 @@ function calculatePath(from, to, cacheLayer, previousWaypoints) {
startCost = (calcNoDiagonals(previousWaypoints) % 2) * 0.5;
}
const nextNodes = new PriorityQueueSet((node1, node2) => node1.node === node2.node, node => node.estimated);
const nextNodes = new PriorityQueueSet(
(node1, node2) => node1.node === node2.node,
node => node.estimated,
);
const previousNodes = new Set();
nextNodes.pushWithPriority(
{
node: getNode(from, cacheLayer),
cost: startCost,
estimated: startCost + estimateCost(from, to),
previous: null
}
);
nextNodes.pushWithPriority({
node: getNode(from, cacheLayer),
cost: startCost,
estimated: startCost + estimateCost(from, to),
previous: null,
});
while (nextNodes.hasNext()) {
// Get node with cheapest estimate
@@ -394,7 +429,7 @@ function calculatePath(from, to, cacheLayer, previousWaypoints) {
node: neighborNode,
cost: currentNode.cost + edge.cost,
estimated: currentNode.cost + edge.cost + estimateCost(neighborNode, to),
previous: currentNode
previous: currentNode,
};
nextNodes.pushWithPriority(neighbor);
}
@@ -412,8 +447,8 @@ function buildPathNodes(lastNode) {
const pathNode = {
node: currentNode.node,
cost: currentNode.cost,
next: previousNode
}
next: previousNode,
};
previousNode = pathNode;
currentNode = currentNode.previous;
}
@@ -444,7 +479,7 @@ function stepCollidesWithWall(from, to, tokenData) {
if (isModuleActive("levels")) {
stepStart.z = tokenData.elevation;
stepEnd.z = tokenData.elevation;
return _levels.testCollision(stepStart, stepEnd, "collision")
return _levels.testCollision(stepStart, stepEnd, "collision");
} else {
return canvas.walls.checkCollision(new Ray(stepStart, stepEnd));
}
@@ -456,8 +491,7 @@ export function wipePathfindingCache() {
GridlessPathfinding.free(pathfinder);
}
gridlessPathfinders.clear();
if (debugGraphics)
debugGraphics.removeChildren().forEach(c => c.destroy());
if (debugGraphics) debugGraphics.removeChildren().forEach(c => c.destroy());
}
export function initializePathfinding() {
@@ -467,22 +501,23 @@ export function initializePathfinding() {
export function startBackgroundCaching(token) {
// Background caching isn't yet supported for gridless scenes
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS)
return;
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) return;
if (game.user.isGM || game.settings.get(settingsKey, "allowPathfinding")) {
cache.startBackgroundCaching(token);
}
}
function paintGriddedPathfindingDebug(firstNode, tokenData) {
if (!CONFIG.debug.dragRuler)
return;
if (!CONFIG.debug.dragRuler) return;
debugGraphics.removeChildren().forEach(c => c.destroy());
let currentNode = firstNode;
while (currentNode) {
let text = new PIXI.Text(currentNode.cost.toFixed(1));
let pixels = getSnapPointForTokenDataObj(getPixelsFromGridPositionObj(currentNode.node), tokenData);
let pixels = getSnapPointForTokenDataObj(
getPixelsFromGridPositionObj(currentNode.node),
tokenData,
);
text.anchor.set(0.5, 1.0);
text.x = pixels.x;
text.y = pixels.y;
@@ -492,8 +527,7 @@ function paintGriddedPathfindingDebug(firstNode, tokenData) {
}
function paintGridlessPathfindingDebug(pathfinder) {
if (!CONFIG.debug.dragRuler)
return;
if (!CONFIG.debug.dragRuler) return;
debugGraphics.removeChildren().forEach(c => c.destroy());
let graphic = new PIXI.Graphics();
+86 -49
View File
@@ -1,6 +1,10 @@
import {currentSpeedProvider, getColorForDistanceAndToken, getRangesFromSpeedProvider} from "./api.js";
import {
currentSpeedProvider,
getColorForDistanceAndToken,
getRangesFromSpeedProvider,
} from "./api.js";
import {getHexSizeSupportTokenGridCenter} from "./compatibility.js";
import {cancelScheduledMeasurement, measure} from "./foundry_imports.js"
import {cancelScheduledMeasurement, measure} from "./foundry_imports.js";
import {getMovementHistory} from "./movement_tracking.js";
import {settingsKey} from "./settings.js";
import {getSnapPointForEntity} from "./util.js";
@@ -8,7 +12,7 @@ import {getSnapPointForEntity} from "./util.js";
export function extendRuler() {
class DragRulerRuler extends Ruler {
// Functions below are overridden versions of functions in Ruler
constructor(user, {color=null}={}) {
constructor(user, {color = null} = {}) {
super(user, {color});
this.previousWaypoints = [];
this.previousLabels = this.addChild(new PIXI.Container());
@@ -24,8 +28,7 @@ export function extendRuler() {
async moveToken(event) {
// Disable moveToken if Drag Ruler is active
if (!this.isDragRuler)
return await super.moveToken(event);
if (!this.isDragRuler) return await super.moveToken(event);
}
toJSON() {
@@ -45,30 +48,30 @@ export function extendRuler() {
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"))
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 {
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={}) {
measure(destination, options = {}) {
if (this.isDragRuler) {
// 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;
if (this.user !== game.user) options.snap = false;
return measure.call(this, destination, options);
}
else {
} else {
return super.measure(destination, options);
}
}
@@ -79,18 +82,20 @@ export function extendRuler() {
}
// The functions below aren't present in the orignal Ruler class and are added by Drag Ruler
dragRulerAddWaypoint(point, options={}) {
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);
this.waypoints
.filter(waypoint => waypoint.isPathfinding)
.forEach(waypoint => (waypoint.isPathfinding = false));
}
dragRulerAddWaypointHistory(waypoints) {
waypoints.forEach(waypoint => waypoint.isPrevious = true);
waypoints.forEach(waypoint => (waypoint.isPrevious = true));
this.waypoints = this.waypoints.concat(waypoints);
for (const waypoint of waypoints) {
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
@@ -102,30 +107,51 @@ export function extendRuler() {
this.labels.removeChildren().forEach(c => c.destroy());
}
dragRulerDeleteWaypoint(event={preventDefault: () => {return}}, options={}) {
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 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);
this._removeWaypoint(
{x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y},
options,
);
game.user.broadcastActivity({ruler: this});
}
else {
} 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
.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}}) {
dragRulerAbortDrag(
event = {
preventDefault: () => {
return;
},
},
) {
const token = this.draggedEntity;
this._endMeasurement();
@@ -139,10 +165,8 @@ export function extendRuler() {
}
async dragRulerRecalculate(tokenIds) {
if (this._state !== Ruler.STATES.MEASURING)
return;
if (tokenIds && !tokenIds.includes(this.draggedEntity.id))
return;
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"))
@@ -155,58 +179,71 @@ export function extendRuler() {
}
static dragRulerGetRaysFromWaypoints(waypoints, destination) {
if ( destination )
waypoints = waypoints.concat([destination]);
if (destination) waypoints = waypoints.concat([destination]);
return waypoints.slice(1).map((wp, i) => {
const ray = new Ray(waypoints[i], wp);
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.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")))
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;
return (
getColorForDistanceAndToken(distance, this.draggedEntity, this.dragRulerRanges) ??
this.color
);
}
dragRulerStart(options, measureImmediately=true) {
dragRulerStart(options, measureImmediately = true) {
const entity = this.draggedEntity;
const isToken = entity instanceof Token;
if (isToken && !currentSpeedProvider.usesRuler(entity))
return;
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))
if (
isToken &&
canvas.grid.isHex &&
game.modules.get("hex-size-support")?.active &&
CONFIG.hexSizeSupport.getAltSnappingFlag(entity)
)
entityCenter = getHexSizeSupportTokenGridCenter(entity);
else
entityCenter = entity.center;
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);
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()
ruler: this.toJSON(),
});
}
}
+105 -95
View File
@@ -1,14 +1,19 @@
import {availableSpeedProviders, currentSpeedProvider, getDefaultSpeedProvider, updateSpeedProvider} from "./api.js";
import {SpeedProvider} from "./speed_provider.js"
import {wipePathfindingCache} from "./pathfinding.js"
import { early_isGM } from "./util.js";
import {
availableSpeedProviders,
currentSpeedProvider,
getDefaultSpeedProvider,
updateSpeedProvider,
} from "./api.js";
import {SpeedProvider} from "./speed_provider.js";
import {wipePathfindingCache} from "./pathfinding.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,
ABORT_DRAG: 2,
});
function delayedReload() {
@@ -20,14 +25,14 @@ export function registerSettings() {
scope: "world",
config: false,
type: String,
default: "fresh install"
})
default: "fresh install",
});
game.settings.register(settingsKey, "clientDataVersion", {
scope: "client",
config: false,
type: String,
default: "fresh install"
default: "fresh install",
});
game.settings.register(settingsKey, "rightClickAction", {
@@ -68,7 +73,7 @@ export function registerSettings() {
config: true,
type: Boolean,
default: true,
})
});
game.settings.register(settingsKey, "showGMRulerToPlayers", {
name: "drag-ruler.settings.showGMRulerToPlayers.name",
@@ -77,7 +82,7 @@ export function registerSettings() {
config: true,
type: Boolean,
default: true,
})
});
game.settings.register(settingsKey, "enableMovementHistory", {
name: "drag-ruler.settings.enableMovementHistory.name",
@@ -134,7 +139,7 @@ export function registerSettings() {
type: String,
default: getDefaultSpeedProvider(),
onChange: updateSpeedProvider,
})
});
game.settings.registerMenu(settingsKey, "speedProviderSettings", {
name: "drag-ruler.settings.speedProviderSettings.name",
@@ -143,7 +148,7 @@ export function registerSettings() {
icon: "fas fa-tachometer-alt",
type: SpeedProviderSettings,
restricted: false,
})
});
}
class SpeedProviderSettings extends FormApplication {
@@ -153,43 +158,44 @@ class SpeedProviderSettings extends FormApplication {
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
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)
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
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
name = game.modules.get(id).data.title;
} else {
name = game.system.data.title;
}
else {
name = game.system.data.title
}
provider.selectTitle = game.i18n.format(`drag-ruler.settings.speedProviderSettings.speedProvider.choices.${type}`, {name})
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
provider.isSelected = provider.id === selectedProvider;
return provider;
});
data.selectedProviderName = data.providers.find(provider => provider.isSelected).selectTitle;
data.providerSelection = {
id: "speedProvider",
@@ -197,122 +203,126 @@ class SpeedProviderSettings extends FormApplication {
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
choices[provider.id] = provider.selectTitle;
return choices;
}, {}),
value: selectedProvider,
isCheckbox: false,
isSelect: true,
isRange: false,
}
return data
};
return data;
}
async _updateObject(event, formData) {
const selectedSpeedProvider = game.user.isGM ? formData.speedProvider : game.settings.get(settingsKey, "speedProvider")
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()
const splitKey = key.split(".", 3);
if (splitKey[0] !== "native") splitKey.shift();
if (splitKey.length >= 2 && splitKey[1] == "color") {
value = parseInt(value.substring(1), 16)
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
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}`
let setting;
if (key === "speedProvider") setting = "speedProvider";
else setting = `speedProviders.${key}`;
// Get the old setting value
const oldValue = game.settings.get(settingsKey, setting)
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)
if (value !== oldValue) game.settings.set(settingsKey, setting, value);
}
// Activate the configured speed provider
updateSpeedProvider()
updateSpeedProvider();
}
activateListeners(html) {
super.activateListeners(html)
html.find("select[name=speedProvider]").change(this.onSpeedProviderChange.bind(this))
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")
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 = ""
document.getElementById(`drag-ruler.provider.${event.currentTarget.value}`).style.display = "";
// Recalculate window height
this.element[0].style.height = null
this.position.height = undefined
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
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"}
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
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")
hint = game.i18n.localize("drag-ruler.settings.speedProviderSettings.color.unreachable.hint");
else
hint = game.i18n.format("drag-ruler.settings.speedProviderSettings.color.hint", {colorName})
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}`)),
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 = []
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)
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)
return settings.concat(colorSettings);
}
+14 -7
View File
@@ -11,8 +11,13 @@ Hooks.once("socketlib.ready", () => {
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))));
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) {
@@ -21,12 +26,15 @@ async function _socketUpdateCombatantDragRulerFlags(combatId, updates) {
const requestedUpdates = updates.length;
updates = updates.filter(update => {
const actor = combat.combatants.get(update._id).actor;
if (!actor)
return false;
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`);
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}};
@@ -40,6 +48,5 @@ export function recalculate(tokens) {
function _socketRecalculate(tokenIds) {
const ruler = canvas.controls.ruler;
if (ruler.isDragRuler)
ruler.dragRulerRecalculate(tokenIds);
if (ruler.isDragRuler) ruler.dragRulerRecalculate(tokenIds);
}
+33 -30
View File
@@ -1,5 +1,5 @@
import {settingsKey} from "./settings.js"
import {getDefaultDashMultiplier, getDefaultSpeedAttribute} from "./systems.js"
import {settingsKey} from "./settings.js";
import {getDefaultDashMultiplier, getDefaultSpeedAttribute} from "./systems.js";
/**
* Base class for all speed providers.
@@ -19,7 +19,7 @@ export class SpeedProvider {
* Implementing this method is required for all speed providers
*/
get colors() {
throw new Error("A SpeedProvider must implement the colors function")
throw new Error("A SpeedProvider must implement the colors function");
}
/**
@@ -31,7 +31,7 @@ export class SpeedProvider {
* Implementing this method is required for all speed providers
*/
getRanges(token) {
throw new Error("A SpeedProvider must implement the getRanges function")
throw new Error("A SpeedProvider must implement the getRanges function");
}
/**
@@ -44,7 +44,7 @@ export class SpeedProvider {
* Implementing this method is optional and only needs to be done if you want to provide custom provider settings
*/
get settings() {
return []
return [];
}
/**
@@ -53,7 +53,7 @@ export class SpeedProvider {
* 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
return 0xff0000;
}
/**
@@ -69,12 +69,12 @@ export class SpeedProvider {
*
* 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={}) {
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))
return costs.reduce((max, current) => Math.max(max, current));
}
/**
@@ -86,7 +86,7 @@ export class SpeedProvider {
* 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
return true;
}
/**
@@ -104,13 +104,14 @@ export class SpeedProvider {
*/
getSetting(settingId) {
try {
return game.settings.get(settingsKey, `speedProviders.${this.id}.setting.${settingId}`)
}
catch (e) {
return game.settings.get(settingsKey, `speedProviders.${this.id}.setting.${settingId}`);
} catch (e) {
if (this.settings.some(setting => setting.id === settingId)) {
throw e
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.`)
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.`,
);
}
}
@@ -120,32 +121,34 @@ export class SpeedProvider {
* This function should neither be called or overridden by speed provider implementations
*/
constructor(id) {
this.id = 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"}
]
{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 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 []
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"}]
const dashMultiplier = this.getSetting("dashMultiplier");
if (!dashMultiplier) return [{range: tokenSpeed, color: "walk"}];
return [
{range: tokenSpeed, color: "walk"},
{range: tokenSpeed * dashMultiplier, color: "dash"},
];
}
get settings() {
@@ -167,7 +170,7 @@ export class GenericSpeedProvider extends SpeedProvider {
config: true,
type: Number,
default: getDefaultDashMultiplier(),
}
]
},
];
}
}
+2 -2
View File
@@ -24,13 +24,13 @@ export function getDefaultSpeedAttribute() {
case "splittermond":
return "actor.derivedValues.speed.value";
}
return ""
return "";
}
export function getDefaultDashMultiplier() {
switch (game.system.id) {
case "swade":
return 0
return 0;
case "dcc":
case "dnd4e":
case "dnd5e":
+72 -76
View File
@@ -1,10 +1,10 @@
import {getPixelsFromGridPosition} from "./foundry_fixes.js"
import {getPixelsFromGridPosition} from "./foundry_fixes.js";
import {findVertexSnapPoint} from "./hex_support.js";
import {disableSnap} from "./keybindings.js";
export function* zip(it1, it2) {
for (let i = 0;i < Math.min(it1.length, it2.length);i++) {
yield [it1[i], it2[i]]
for (let i = 0; i < Math.min(it1.length, it2.length); i++) {
yield [it1[i], it2[i]];
}
}
@@ -17,7 +17,7 @@ export function* enumeratedZip(it1, it2) {
}
export function* iterPairs(l) {
for (let i = 1;i < l.length;i++) {
for (let i = 1; i < l.length; i++) {
yield [l[i - 1], l[i]];
}
}
@@ -29,13 +29,14 @@ export function sum(arr) {
export function buildSnapPointTokenData(token) {
const tokenData = {
width: token.data.width,
height: token.data.height
height: token.data.height,
};
if (isModuleActive("hex-size-support")) {
tokenData.hexSizeSupport = {};
tokenData.hexSizeSupport.altSnappingFlag = CONFIG.hexSizeSupport.getAltSnappingFlag(token);
tokenData.hexSizeSupport.altOrientationFlag = CONFIG.hexSizeSupport.getAltOrientationFlag(token);
tokenData.hexSizeSupport.altOrientationFlag =
CONFIG.hexSizeSupport.getAltOrientationFlag(token);
tokenData.hexSizeSupport.borderSize = token.document.getFlag("hex-size-support", "borderSize");
}
@@ -58,27 +59,21 @@ function getSnapPointForTokenData(x, y, tokenData) {
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)
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))
}
}
else {
} 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;
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
@@ -101,11 +96,9 @@ function getSnapPointForTokenData(x, y, tokenData) {
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) {
} else if (Math.round(tokenData.height) % 2 === 1 || tokenData.height < 1) {
snapY = centerY;
}
else {
} else {
snapY = centerY + canvas.grid.h / 2;
}
return new PIXI.Point(snapX, snapY);
@@ -124,16 +117,13 @@ export function getSnapPointForMeasuredTemplate(x, y) {
export function getSnapPointForEntity(x, y, entity) {
const isToken = entity instanceof Token;
if (isToken)
return getSnapPointForToken(x, y, entity);
else
return getSnapPointForMeasuredTemplate(x, y);
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;
if (!layer) return false;
const area = getAreaFromPositionAndShape(position, shape);
for (const space of area) {
const [x, y] = getPixelsFromGridPosition(space.x, space.y);
@@ -147,22 +137,19 @@ export function getAreaFromPositionAndShape(position, shape) {
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.even) shiftedRow = 1;
else shiftedRow = 0;
if (canvas.grid.grid.options.columns) {
if (space.x % 2 !== 0 && position.x % 2 !== shiftedRow) {
y += 1;
}
}
else {
} else {
if (space.y % 2 !== 0 && position.y % 2 !== shiftedRow) {
x += 1;
}
}
}
return {x, y}
return {x, y};
});
}
@@ -170,40 +157,53 @@ export function getTokenShape(token) {
return getTokenShapeForTokenData(buildSnapPointTokenData(token), token.scene);
}
export function getTokenShapeForTokenData(tokenData, scene=canvas.scene) {
export function getTokenShapeForTokenData(tokenData, scene = canvas.scene) {
if (scene.data.gridType === CONST.GRID_TYPES.GRIDLESS) {
return [{x: 0, y: 0}]
}
else if (scene.data.gridType === 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 [{x: 0, y: 0}];
} else if (scene.data.gridType === 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 {
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}]);
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}]);
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}])
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);
shape.forEach(space => (space.y *= -1));
if (canvas.grid.grid.options.columns)
shape = shape.map(space => {return {x: space.y, y: space.x}});
shape = shape.map(space => {
return {x: space.y, y: space.x};
});
return shape;
}
else {
} else {
return [{x: 0, y: 0}];
}
}
@@ -213,11 +213,10 @@ export function getTokenSize(token) {
let w, h;
const hexSizeSupportBorderSize = token.data.flags["hex-size-support"]?.borderSize;
if (hexSizeSupportBorderSize > 0) {
w = h = hexSizeSupportBorderSize
}
else {
w = token.data.width
h = token.data.height
w = h = hexSizeSupportBorderSize;
} else {
w = token.data.width;
h = token.data.height;
}
return {w, h};
}
@@ -226,7 +225,7 @@ export function getTokenSize(token) {
// 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
return waypoints;
}
const tokenSize = getTokenSize(token);
@@ -237,21 +236,17 @@ export function applyTokenSizeOffset(waypoints, token) {
if (canvas.grid.grid.options.columns) {
if (tokenSize.w % 2 === 0) {
waypointOffset.x = canvas.grid.w / 2;
if (!isAltOrientation)
waypointOffset.x *= -1;
if (!isAltOrientation) waypointOffset.x *= -1;
}
}
else {
} else {
if (tokenSize.h % 2 === 0) {
waypointOffset.y = canvas.grid.h / 2;
if (isAltOrientation)
waypointOffset.y *= -1;
if (isAltOrientation) waypointOffset.y *= -1;
}
}
}
// If hex size support isn't active leave the waypoints like they are
}
else {
} else {
if (tokenSize.w % 2 === 0) {
waypointOffset.x = canvas.grid.w / 2;
}
@@ -260,7 +255,7 @@ export function applyTokenSizeOffset(waypoints, token) {
}
}
return waypoints.map(w => new PIXI.Point(w.x + waypointOffset.x, w.y + waypointOffset.y))
return waypoints.map(w => new PIXI.Point(w.x + waypointOffset.x, w.y + waypointOffset.y));
}
export function setSnapParameterOnOptions(sourceObject, options) {
@@ -269,8 +264,7 @@ export function setSnapParameterOnOptions(sourceObject, options) {
options.snapOverrideActive = true;
options.snap = sourceObject.snapOverride.snap;
sourceObject.snapOverride = undefined; // remove it to prevent any lingering data issues
}
else {
} else {
options.snap = !disableSnap;
}
}
@@ -280,7 +274,9 @@ export function isClose(a, b, delta) {
}
export function getMeasurePosition() {
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens);
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;
@@ -294,5 +290,5 @@ export function early_isGM() {
}
export function isModuleActive(moduleName) {
return game.modules.get(moduleName)?.active
return game.modules.get(moduleName)?.active;
}