Compare commits

..

6 Commits

Author SHA1 Message Date
Manuel Vögele 0588d3ef8b Pathfinding prerelease 2021-07-19 22:40:02 +02:00
Manuel Vögele b084d25cb6 Add an option to do pathfinding automatically 2021-07-19 22:39:48 +02:00
Manuel Vögele 654847563f Reduce broken caching 2021-07-19 22:04:25 +02:00
Manuel Vögele baca42a2f8 Broadcast pathfinding waypoints to other players 2021-07-19 22:04:25 +02:00
Manuel Vögele 68ee098e61 Penalize diagonals minimally to disincentivise using unnecessary diagonals 2021-07-19 22:04:25 +02:00
Manuel Vögele 3300007f93 Initial working pathfinding impl 2021-07-19 22:04:25 +02:00
19 changed files with 275 additions and 472 deletions
-41
View File
@@ -1,44 +1,3 @@
## 1.9.0
### New features
- On Gridless scenes, tokens can now snap to their speed limits, to make full usage of a token's movement speed easier. This feature can be temporarily disabled by pressing Shift during drag and can be disabled completely in the settings.
### Bugfixes
- Non-square tokens (e.g. 2x1) now work correctly on square grids
- When modifying difficult terrain that a token has already moved over, this the movement history of the token won't change anymore (this was a regression introduced in 1.8.0)
- Fixed a bug that prevented pausing/unpausing the game when no scene was active
### API
- Added `dragRuler.getColorForDistanceAndToken` API endpoint that allows other modules to receive the highlight color for a specified distance with a given token.
## 1.8.2
### Compatibility
- The generic speed provider defaults have been updated for lance 1.0 (thanks to BoltsJ!)
- Eliminated a deprecation warning when both Drag Ruler and Hex Size Support are enabled (thanks Argonius-Angelus!)
## 1.8.1
### Bugfixes
- Fixed a bug where the function that was bound to the spacebar key wouldn't work correctly when the "Toggle Snap To Grid" module was enabled
### Translation
- Updated Spanish translation (thanks to Viriato139ac#342)
## 1.8.0
### New features
- Pressing escape during a drag now cancels the drag
- Undoing a movement via Ctrl+Z will now also remove that movement from Drag Ruler's movement history
- Drag Ruler can now configured to stay disabled by default when a Token/Template is being dragged. In that case it will activate once the button to place a waypoint is being pressed.
### Bugfixes
- Fixed a bug that caused the ruler to snap to grid when a waypoint was deleted while shift was being pressed (thanks to Michael Clavell!)
- Fixed a bug that could leave behind a single waypoint on the canvas when canceling a dragging operation while moving the mouse
### Compatibility
- Drag Ruler is now compatible with the "Toggle Snap To Grid" module (thanks to Michael Clavell!)
## 1.7.7
### Compatibility
- Updated the default settings for the swade game system. The new default speed attribute points to a speed value that gets adjusted for wounds.
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 Manuel Vögele
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-6
View File
@@ -37,16 +37,10 @@ Drag Ruler will work with all Foundry VTT game systems. However some game system
The game systems that offer Drag Ruler integration are:
- Cypher System (starting with version 1.13.0)
- GURPS 4th Edition Game Aid (Unofficial) (starting with version 0.9.1)
- Ironclaw Second Edition (starting with version 0.2.2)
- Lancer (via the module [Lancer Speed Provider](https://foundryvtt.com/packages/lancer-speed-provider))
- Pathfinder 1 (starting with version 0.77.3)
- Pathfinder 2e (via the module [PF2E Drag Ruler Integration](https://foundryvtt.com/packages/pf2e-dragruler/))
- Shadowrun 5th Edition (via the module [Drag Ruler Integration for Shadowrun 5E](https://foundryvtt.com/packages/drag-ruler-integration-for-shadowrun-5e))
- Starfinder (via the module [Starfinder Drag Ruler Integration](https://foundryvtt.com/packages/starfinder-drag-ruler))
- Stargate RPG (starting with version 1.6.0)
- Tagmar RPG (starting with version 1.1.4)
- TheWitcherTRPG (starting with version 0.0.62)
- Tormenta20 (starting with version 1.1.37)
- Shadow of the Demon Lord (starting with version 1.7.15)
- Wasteland Ventures (starting with version 0.1.0)
-8
View File
@@ -34,10 +34,6 @@
"name": "Geschwindigkeit von Spielercharakteren für jeden anzeigen",
"hint": "Wenn diese Einstellung aktiviert ist wird die Färbung der hervorgehobenen Felder bei Spielercharakteren allen Spielern angezeigt, selbst wenn diese keinen Zugriff auf den Charakterbogen haben."
},
"autoStartMeasurement": {
"name": "Automatisch mit Messen beginnen",
"hint": "Wenn diese Option aktiviert ist beginnt Drag Ruler mit dem Messen sobald das Token bewegt wird. Wenn die Option deaktiviert ist bleibt Drag Ruler deaktiviert bis der Knopf zum platzieren eines Wegpunkts betätigt wird."
},
"enableMovementHistory": {
"name": "Bewegungsverlauf während des Kampfes aktivieren",
"hint": "Wenn diesen Option aktiviert ist merkt sich Drag Ruler die Bewegungen, die während einer Runde eines Kampfes gemacht wurden und zeigt diese an, wenn das Token weiterbewegt wird."
@@ -81,10 +77,6 @@
"swapSpacebarRightClick": {
"name": "Leertaste und Rechtsklick tauschen",
"hint": "Die Funktionen der Leertaste und des Rechtsklicks sind, während eine Spielfigur bewegt wird, vertauscht. Wenn diese Option aktiviert wird können mit Rechtsklick Wegpunkte gesetzt werden und mit der Leertaste werden sie wieder gelöscht."
},
"useGridlessraster": {
"name": "Geschwindigkeitsraster aktivieren",
"hint": "Lässt Spielfiguren auf gitterlosen Szenen an deren Geschwindigkeitsgrenzen einrasten. Dies kann vorübergehend deaktiviert werden, indem während des ziehens die Umschalttaste gedrückt wird."
}
}
}
+3 -7
View File
@@ -34,9 +34,9 @@
"name": "Show PC speed to everyone",
"hint": "If enabled the coloring based on actor speed for player characters will shown to everyone, even if they don't have observer permission for the character sheet."
},
"autoStartMeasurement": {
"name": "Automatically start measuring",
"hint": "If enabled, Drag Ruler will start measuring as soon as the token is being dragged. If disabled, Drag Ruler will remain inactive and will only start measuring once the button to add a waypoint is being pressed."
"autoPathfinding": {
"name": "Pathfinding by default",
"hint": "If enabled, dragging a token will automatically use a pathfinding ruler"
},
"enableMovementHistory": {
"name": "Enable movement history during combat",
@@ -81,10 +81,6 @@
"swapSpacebarRightClick": {
"name": "Swap spacebar and right click",
"hint": "Swaps the functions of spacebar and right click during dragging. If enabled right click will place waypoints and spacebar will delete them"
},
"useGridlessRaster": {
"name": "Use speed based snapping",
"hint": "On Gridless scenes, this makes tokens snap to the token's speed ranges. This can be temporarily disabled by pressing Shift during drag."
}
}
}
-4
View File
@@ -38,10 +38,6 @@
"name": "Mostrar velocidad de los PJs a todo el mundo",
"hint": "Si se habilita, se mostrará a todo el mundo los códigos de colores de las rutas de los PJs, incluso si no tienen permisos de observador para ese personaje"
},
"autoStartMeasurement": {
"name": "Comenzar a medir automáticamente",
"hint": "Si se habilita, Drag Ruler comenzará a medir en cuanto se comience a arrastrar un icono. Si se deshabilita, Drag Ruler permanecerá inactivo y comenzará a medir únicamente cuando se presione el botón configurado para añadir un nuevo punto de ruta"
},
"enableMovementHistory": {
"name": "Habilitar historial de movimiento durante el combate",
"hint": "Si se habilita, Drag Ruler recordará la ruta que ha seguido un icono en su turno y la mostrará al seleccionarlo de nuevo"
+3 -3
View File
@@ -2,9 +2,9 @@
"name": "drag-ruler",
"title": "Drag Ruler",
"description": "When dragging a token displays a ruler showing how far you've moved that token.",
"version": "1.9.0",
"version": "1.7.7-prerelease1",
"minimumCoreVersion" : "0.8.5",
"compatibleCoreVersion" : "0.8.9",
"compatibleCoreVersion" : "0.8.8",
"authors": [
{
"name": "Manuel Vögele",
@@ -59,7 +59,7 @@
],
"socket": true,
"url": "https://github.com/manuelVo/foundryvtt-drag-ruler",
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/v1.9.0.zip",
"download": "https://codeload.github.com/manuelVo/foundryvtt-drag-ruler/zip/refs/heads/prerelease",
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/master/module.json",
"readme": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/README.md",
"changelog": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/CHANGELOG.md",
+4 -19
View File
@@ -103,12 +103,12 @@ export function getUnreachableColorFromSpeedProvider() {
}
}
export function getCostFromSpeedProvider(token, area, options) {
export function getCostFromSpeedProvider(token, area) {
try {
if (currentSpeedProvider instanceof Function) {
return SpeedProvider.prototype.getCostForStep.call(undefined, token, area, options);
return SpeedProvider.prototype.getCostForStep.call(undefined, token, area);
}
return currentSpeedProvider.getCostForStep(token, area, options);
return currentSpeedProvider.getCostForStep(token, area);
}
catch (e) {
console.error(e);
@@ -116,26 +116,11 @@ export function getCostFromSpeedProvider(token, area, options) {
}
}
export function getColorForDistanceAndToken(distance, token, ranges=null) {
if (!ranges) {
ranges = getRangesFromSpeedProvider(token);
}
if (ranges.length === 0)
return this.color;
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 shape = getTokenShape(token);
const distances = measureDistances(segments, token, shape, {enableTerrainRuler: terrainRulerAvailable});
const distances = measureDistances(segments, token, shape);
// Sum up the distances
return distances.reduce((acc, val) => acc + val, 0);
}
+11 -9
View File
@@ -1,4 +1,5 @@
import {getCostFromSpeedProvider} from "./api.js";
import {getColorForDistance} from "./main.js"
import {settingsKey} from "./settings.js";
import {getAreaFromPositionAndShape, highlightTokenShape} from "./util.js";
@@ -9,32 +10,30 @@ export function getHexSizeSupportTokenGridCenter(token) {
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);
const color = getColorForDistance.call(this, startDistance, space.distance)
highlightTokenShape.call(this, space, tokenShape, color, alpha)
}
}
export function measureDistances(segments, entity, shape, options={}) {
const isToken = entity instanceof Token;
const opts = duplicate(options)
if (opts.enableTerrainRuler) {
if (!opts.gridSpaces)
opts.gridSpaces = true;
const terrainRulerAvailable = isToken && game.modules.get("terrain-ruler")?.active && (!game.modules.get("TerrainLayer")?.active || canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS);
if (terrainRulerAvailable) {
const firstNewSegmentIndex = segments.findIndex(segment => !segment.ray.dragRulerVisitedSpaces);
const previousSegments = segments.slice(0, firstNewSegmentIndex);
const newSegments = segments.slice(firstNewSegmentIndex);
const distances = previousSegments.map(segment => segment.ray.dragRulerVisitedSpaces[segment.ray.dragRulerVisitedSpaces.length - 1].distance);
previousSegments.forEach(segment => segment.ray.terrainRulerVisitedSpaces = duplicate(segment.ray.dragRulerVisitedSpaces));
opts.costFunction = (x, y, costOptions={}) => { return getCostFromSpeedProvider(entity, getAreaFromPositionAndShape({x, y}, shape), costOptions); }
opts.costFunction = (x, y) => getCostFromSpeedProvider(entity, getAreaFromPositionAndShape({x, y}, shape), {x, y});
if (previousSegments.length > 0)
opts.terrainRulerInitialState = previousSegments[previousSegments.length - 1].ray.dragRulerFinalState;
return distances.concat(terrainRuler.measureDistances(newSegments, opts));
}
else {
// If another module wants to enable grid measurements but disable grid highlighting,
// manually set the *duplicate* option's gridSpaces value to true for the Foundry logic to work properly
if(!opts.ignoreGrid) {
opts.gridSpaces = true;
}
return canvas.grid.measureDistances(segments, opts);
}
}
@@ -62,6 +61,9 @@ export function checkDependencies() {
if (game.modules.get("enhanced-terrain-layer")?.active) {
enabledTerrainModule = game.modules.get("enhanced-terrain-layer").data.title;
}
else if (game.modules.get("TerrainLayer")?.active) {
enabledTerrainModule = game.modules.get("TerrainLayer").data.title;
}
if (enabledTerrainModule) {
new Dialog({
title: game.i18n.localize("drag-ruler.dependencies.terrain-ruler.title"),
+18
View File
@@ -16,3 +16,21 @@ export function getGridPositionFromPixels(xPixel, yPixel) {
return [y, x]
return [x, y]
}
export function getGridPositionFromPixelsObj(o) {
const r = {};
[r.x, r.y] = getGridPositionFromPixels(o.x, o.y);
return r;
}
export function getPixelsFromGridPositionObj(o) {
const r = {};
[r.x, r.y] = getPixelsFromGridPosition(o.x, o.y);
return r;
}
export function getCenterFromGridPositionObj(o) {
const r = getPixelsFromGridPositionObj(o);
[r.x, r.y] = canvas.grid.getCenter(r.x, r.y);
return r;
}
+36 -26
View File
@@ -1,7 +1,10 @@
import {highlightMeasurementTerrainRuler, measureDistances} from "./compatibility.js";
import {getGridPositionFromPixels} from "./foundry_fixes.js";
import {getCenterFromGridPositionObj, getGridPositionFromPixels, getGridPositionFromPixelsObj} from "./foundry_fixes.js";
import {Line} from "./geometry.js";
import {getColorForDistance} from "./main.js"
import {trackRays} from "./movement_tracking.js"
import {find_path} from "./pathfinding.js";
import {settingsKey} from "./settings.js";
import {recalculate} from "./socket.js";
import {applyTokenSizeOffset, getSnapPointForEntity, getSnapPointForToken, getTokenShape, highlightTokenShape, zip} from "./util.js";
@@ -76,6 +79,7 @@ async function animateEntities(entities, draggedEntity, draggedRays, wasPaused)
this.cancelMovement = false;
for (let i = startWaypoint;i < entityAnimationData[0].rays.length; i++) {
if (!wasPaused && game.paused) break;
const entityPaths = entityAnimationData.map(({entity, rays, dx, dy}) => {
const ray = rays[i];
@@ -100,6 +104,7 @@ async function animateEntities(entities, draggedEntity, draggedRays, wasPaused)
trackRays(entities, entityAnimationData.map(({rays}) => rays)).then(() => recalculate(entities));
}
function calculateEntityOffset(entityA, entityB) {
return {x: entityA.data.x - entityB.data.x, y: entityA.data.y - entityB.data.y};
}
@@ -115,6 +120,7 @@ export function onMouseMove(event) {
if (this._state === Ruler.STATES.MOVING) return;
// Extract event data
const destination = {x: event.data.destination.x + this.rulerOffset.x, y: event.data.destination.y + this.rulerOffset.y}
// Hide any existing Token HUD
@@ -130,10 +136,12 @@ function scheduleMeasurement(destination, event) {
const mt = event._measureTime || 0;
const originalEvent = event.data.originalEvent;
if (Date.now() - mt > measurementInterval) {
this.measure(destination, {snap: !originalEvent.shiftKey});
this.measure(destination, {snap: !originalEvent.shiftKey, pathfinding: game.settings.get(settingsKey, "autoPathfinding") != game.keyboard.isDown("y")});
event._measureTime = Date.now();
this._state = Ruler.STATES.MEASURING;
cancelScheduledMeasurement.call(this);
window.clearTimeout(this.deferredMeasurementTimeout);
this.deferredMeasurementTimeout = undefined;
this.deferredMeasurementResolve?.();
}
else {
this.deferredMeasurementData = {destination, event};
@@ -144,37 +152,39 @@ function scheduleMeasurement(destination, event) {
}
}
export function cancelScheduledMeasurement() {
window.clearTimeout(this.deferredMeasurementTimeout);
this.deferredMeasurementTimeout = undefined;
this.deferredMeasurementResolve?.();
}
// This is a modified version of Ruler.measure form foundry 0.7.9
export function measure(destination, options={}) {
export function measure(destination, {gridSpaces=true, snap=false, pathfinding=false} = {}) {
const isToken = this.draggedEntity instanceof Token;
if (isToken && !this.draggedEntity.isVisible)
return []
if (options.snap) {
if (snap) {
destination = getSnapPointForEntity(destination.x, destination.y, this.draggedEntity);
}
if(options.gridSpaces === undefined) {
options.gridSpaces = canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS;
// Remove waypoints generated by pathfinding
this.waypoints = this.waypoints.filter(waypoint => !waypoint.isPathfinding);
if (isToken && pathfinding) {
let path = find_path(getGridPositionFromPixelsObj(this.waypoints[this.waypoints.length - 1]), getGridPositionFromPixelsObj(destination));
if (path) {
path = path.map(point => getCenterFromGridPositionObj(point));
path.forEach(point => point.isPathfinding = true);
this.waypoints = this.waypoints.concat(path);
}
else {
// Don't show a path if the pathfinding yields no result to show the user that the destination is unreachable
destination = this.waypoints[this.waypoints.length - 1];
}
}
if(options.ignoreGrid === undefined) {
options.ignoreGrid = false;
}
options.enableTerrainRuler = isToken && game.modules.get("terrain-ruler")?.active;
const terrainRulerAvailable = isToken && game.modules.get("terrain-ruler")?.active && (!game.modules.get("TerrainLayer")?.active || canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS);
const waypoints = this.waypoints.concat([destination]);
// Move the waypoints to the center of the grid if a size is used that measures from edge to edge
const centeredWaypoints = isToken ? applyTokenSizeOffset(waypoints, this.draggedEntity) : duplicate(waypoints);
// Foundries native ruler requires the waypoints to sit in the dead center of the square to work properly
if (!options.enableTerrainRuler && !options.ignoreGrid)
if (!terrainRulerAvailable)
centeredWaypoints.forEach(w => [w.x, w.y] = canvas.grid.getCenter(w.x, w.y));
const r = this.ruler;
@@ -208,7 +218,7 @@ export function measure(destination, options={}) {
const shape = isToken ? getTokenShape(this.draggedEntity) : null;
// Compute measured distance
const distances = measureDistances(centeredSegments, this.draggedEntity, shape, options);
const distances = measureDistances(centeredSegments, this.draggedEntity, shape, {gridSpaces});
let totalDistance = 0;
for (let [i, d] of distances.entries()) {
@@ -227,8 +237,8 @@ export function measure(destination, options={}) {
// Draw measured path
r.clear();
let rulerColor
if (!options.gridSpaces || canvas.grid.type === CONST.GRID_TYPES.GRIDLESS)
rulerColor = this.dragRulerGetColorForDistance(totalDistance);
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS)
rulerColor = getColorForDistance.call(this, totalDistance)
else
rulerColor = this.color
for (const [s, cs] of zip(segments.reverse(), centeredSegments.reverse())) {
@@ -263,8 +273,8 @@ export function measure(destination, options={}) {
}
// Highlight grid positions
if (isToken && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS && options.gridSpaces) {
if (options.enableTerrainRuler)
if (isToken && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS) {
if (terrainRulerAvailable)
highlightMeasurementTerrainRuler.call(this, cs.ray, cs.startDistance, shape, opacityMultiplier)
else
highlightMeasurementNative.call(this, cs.ray, cs.startDistance, shape, opacityMultiplier);
@@ -300,7 +310,7 @@ export function highlightMeasurementNative(ray, startDistance, tokenShape=[{x: 0
// Highlight the grid position
let [xg, yg] = canvas.grid.grid.getPixelsFromGridPosition(x1, y1);
const subDistance = canvas.grid.measureDistances([{ray: new Ray(ray.A, {x: xg, y: yg})}], {gridSpaces: true})[0]
const color = this.dragRulerGetColorForDistance(startDistance + subDistance);
const color = dragRuler.getColorForDistance.call(this, startDistance, subDistance)
const snapPoint = getSnapPointForToken(...canvas.grid.getTopLeft(x, y), this.draggedEntity);
const [snapX, snapY] = getGridPositionFromPixels(snapPoint.x + 1, snapPoint.y + 1);
@@ -313,7 +323,7 @@ export function highlightMeasurementNative(ray, startDistance, tokenShape=[{x: 0
let [x1h, y1h] = canvas.grid.grid.getGridPositionFromPixels(x, y);
let [xgh, ygh] = canvas.grid.grid.getPixelsFromGridPosition(x1h, y1h);
const subDistance = canvas.grid.measureDistances([{ray: new Ray(ray.A, {x: xgh, y: ygh})}], {gridSpaces: true})[0]
const color = this.dragRulerGetColorForDistance(startDistance + subDistance);
const color = dragRuler.getColorForDistance.call(this, startDistance, subDistance)
const snapPoint = getSnapPointForToken(...canvas.grid.getTopLeft(x, y), this.draggedEntity);
const [snapX, snapY] = getGridPositionFromPixels(snapPoint.x + 1, snapPoint.y + 1);
highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color, alpha);
+43 -169
View File
@@ -1,15 +1,15 @@
"use strict"
import {currentSpeedProvider, getColorForDistanceAndToken, getMovedDistanceFromToken, getRangesFromSpeedProvider, initApi, registerModule, registerSystem} from "./api.js";
import {checkDependencies, getHexSizeSupportTokenGridCenter, highlightMeasurementTerrainRuler} from "./compatibility.js";
import {currentSpeedProvider, getMovedDistanceFromToken, getRangesFromSpeedProvider, getUnreachableColorFromSpeedProvider, initApi, registerModule, registerSystem} from "./api.js"
import {checkDependencies, getHexSizeSupportTokenGridCenter} from "./compatibility.js";
import {moveEntities, onMouseMove} from "./foundry_imports.js"
import {performMigrations} from "./migration.js"
import {DragRulerRuler} from "./ruler.js";
import {getMovementHistory, removeLastHistoryEntryIfAt, resetMovementHistory} from "./movement_tracking.js";
import {getMovementHistory, resetMovementHistory} from "./movement_tracking.js";
import {registerSettings, settingsKey} from "./settings.js"
import {recalculate} from "./socket.js";
import {SpeedProvider} from "./speed_provider.js"
import {isClose, setSnapParameterOnOptions} from "./util.js";
import { wipe_cache } from "./pathfinding.js";
Hooks.once("init", () => {
registerSettings()
@@ -17,12 +17,11 @@ Hooks.once("init", () => {
hookDragHandlers(Token);
hookDragHandlers(MeasuredTemplate);
hookKeyboardManagerFunctions()
hookLayerFunctions();
Ruler = DragRulerRuler;
window.dragRuler = {
getColorForDistanceAndToken,
getColorForDistance,
getMovedDistanceFromToken,
registerModule,
registerSystem,
@@ -42,7 +41,7 @@ Hooks.on("canvasReady", () => {
ruler.draggedEntity = null;
Object.defineProperty(ruler, "isDragRuler", {
get: function isDragRuler() {
return Boolean(this.draggedEntity) && this._state !== Ruler.STATES.INACTIVE;
return Boolean(this.draggedEntity); // If draggedEntity is set this is a drag ruler
}
})
})
@@ -66,8 +65,6 @@ function hookDragHandlers(entityType) {
const originalDragLeftMoveHandler = entityType.prototype._onDragLeftMove
entityType.prototype._onDragLeftMove = function (event) {
if (entityType === Token)
applyGridlessSnapping.call(this, event);
originalDragLeftMoveHandler.call(this, event)
onEntityLeftDragMove.call(this, event)
}
@@ -96,32 +93,12 @@ function hookKeyboardManagerFunctions() {
}
}
function hookLayerFunctions() {
const originalTokenLayerUndoHistory = TokenLayer.prototype.undoHistory;
TokenLayer.prototype.undoHistory = function () {
const historyEntry = this.history[this.history.length - 1];
return originalTokenLayerUndoHistory.call(this).then((returnValue) => {
if (historyEntry.type === "update") {
for (const entry of historyEntry.data) {
const token = canvas.tokens.get(entry._id);
removeLastHistoryEntryIfAt(token, entry.x, entry.y);
}
}
return returnValue;
});
}
}
function handleKeys(event, key, up) {
if (event.repeat || this.hasFocus)
return false
const lowercaseKey = key.toLowerCase();
if (lowercaseKey === "x") return onKeyX(up)
if (lowercaseKey === "shift") return onKeyShift(up)
if (lowercaseKey === "space") return onKeySpace(up);
if (lowercaseKey === "escape") return onKeyEscape(up);
if (key.toLowerCase() === "x") return onKeyX(up)
if (key.toLowerCase() === "shift") return onKeyShift(up)
return false
}
@@ -149,39 +126,10 @@ function onKeyShift(up) {
ruler.measure(measurePosition, {snap: up})
}
function onKeySpace(up) {
const ruler = canvas.controls.ruler;
// Ruler can end up being undefined here if no canvas is active
if (!ruler?.draggedEntity)
return false;
if (ruler._state !== Ruler.STATES.INACTIVE)
return false;
const swapSpacebarRightClick = game.settings.get(settingsKey, "swapSpacebarRightClick");
let options = {};
setSnapParameterOnOptions(ruler, options);
if (!up) {
if (swapSpacebarRightClick)
ruler.dragRulerAbortDrag();
else
startDragRuler.call(ruler.draggedEntity, options);
}
return true;
}
function onKeyEscape(up) {
const ruler = canvas.controls.ruler;
if (!ruler.draggedEntity)
return false;
if (!up)
ruler.dragRulerAbortDrag();
return true;
}
function onEntityLeftDragStart(event) {
const isToken = this instanceof Token;
if (isToken && !currentSpeedProvider.usesRuler(this))
return
const ruler = canvas.controls.ruler
ruler.draggedEntity = this;
let entityCenter;
@@ -189,33 +137,13 @@ function onEntityLeftDragStart(event) {
entityCenter = getHexSizeSupportTokenGridCenter(this);
else
entityCenter = this.center;
ruler.rulerOffset = {x: entityCenter.x - event.data.origin.x, y: entityCenter.y - event.data.origin.y};
if (game.settings.get(settingsKey, "autoStartMeasurement")) {
let options = {};
setSnapParameterOnOptions(ruler, options);
startDragRuler.call(this, options, false);
}
}
function startDragRuler(options, measureImmediately=true) {
const isToken = this instanceof Token;
if (isToken && !currentSpeedProvider.usesRuler(this))
return;
const ruler = canvas.controls.ruler;
ruler.clear();
wipe_cache();
ruler._state = Ruler.STATES.STARTING;
let entityCenter;
if (isToken && canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(this))
entityCenter = getHexSizeSupportTokenGridCenter(this);
else
entityCenter = this.center;
ruler.rulerOffset = {x: entityCenter.x - event.data.origin.x, y: entityCenter.y - event.data.origin.y};
if (isToken && game.settings.get(settingsKey, "enableMovementHistory"))
ruler.dragRulerAddWaypointHistory(getMovementHistory(this));
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);
ruler.dragRulerAddWaypoint(entityCenter, false);
}
function onEntityLeftDragMove(event) {
@@ -226,10 +154,8 @@ function onEntityLeftDragMove(event) {
function onEntityDragLeftDrop(event) {
const ruler = canvas.controls.ruler
if (!ruler.isDragRuler) {
ruler.draggedEntity = undefined;
if (!ruler.isDragRuler)
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
@@ -244,95 +170,43 @@ function onEntityDragLeftDrop(event) {
function onEntityDragLeftCancel(event) {
// This function is invoked by right clicking
const ruler = canvas.controls.ruler
if (!ruler.draggedEntity || ruler._state === Ruler.STATES.MOVING)
if (!ruler.isDragRuler || ruler._state === Ruler.STATES.MOVING)
return false
const swapSpacebarRightClick = game.settings.get(settingsKey, "swapSpacebarRightClick");
let options = {};
setSnapParameterOnOptions(ruler, options);
if (ruler._state === Ruler.STATES.INACTIVE) {
if (!swapSpacebarRightClick)
return false;
startDragRuler.call(this, options);
event.preventDefault();
}
else if (ruler._state === Ruler.STATES.MEASURING) {
if (!swapSpacebarRightClick) {
ruler.dragRulerDeleteWaypoint(event, options);
if (ruler._state === Ruler.STATES.MEASURING) {
if (!game.settings.get(settingsKey, "swapSpacebarRightClick")) {
ruler.dragRulerDeleteWaypoint(event);
}
else {
event.preventDefault();
ruler.dragRulerAddWaypoint(ruler.destination, options);
event.preventDefault()
const snap = !event.shiftKey
ruler.dragRulerAddWaypoint(ruler.destination, snap);
}
}
return true
}
function applyGridlessSnapping(event) {
const ruler = canvas.controls.ruler;
if (!game.settings.get(settingsKey, "useGridlessRaster"))
return;
if (!ruler.isDragRuler)
return;
if (game.keyboard.isDown("Shift"))
return;
if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS)
return;
const rasterWidth = 35 / canvas.stage.scale.x;
const tokenX = event.data.destination.x;
const tokenY = event.data.destination.y;
const destination = {x: tokenX + ruler.rulerOffset.x, y: tokenY + ruler.rulerOffset.y};
const ranges = getRangesFromSpeedProvider(ruler.draggedEntity);
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active;
if (terrainRulerAvailable) {
const segments = Ruler.dragRulerGetRaysFromWaypoints(ruler.waypoints, destination).map(ray => {return {ray}});
const pinpointDistances = new Map();
for (const range of ranges) {
pinpointDistances.set(range.range, null);
}
terrainRuler.measureDistances(segments, {pinpointDistances});
const targetDistance = Array.from(pinpointDistances.entries())
.filter(([_key, val]) => val)
.reduce((value, current) => value[0] > current[0] ? value : current, [0, null]);
const rasterLocation = targetDistance[1];
if (rasterLocation) {
const deltaX = destination.x - rasterLocation.x;
const deltaY = destination.y - rasterLocation.y;
const rasterDistance = Math.hypot(deltaX, deltaY);
if (rasterDistance < rasterWidth) {
event.data.destination.x = rasterLocation.x - ruler.rulerOffset.x;
event.data.destination.y = rasterLocation.y - ruler.rulerOffset.y;
}
}
export function getColorForDistance(startDistance, subDistance=0) {
if (!this.isDragRuler)
return this.color
if (!this.draggedEntity.actor) {
return this.color;
}
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}});
origin = segments.pop().ray.A;
waypointDistance = canvas.grid.measureDistances(segments).reduce((a, b) => a + b);
origin = {x: origin.x - ruler.rulerOffset.x, y: origin.y - ruler.rulerOffset.y};
}
const deltaX = tokenX - origin.x;
const deltaY = tokenY - origin.y;
const distance = Math.hypot(deltaX, deltaY);
// targetRange will be the largest range that's still smaller than distance
let targetDistance = ranges
.map(range => range.range)
.map(range => range - waypointDistance)
.map(range => range * canvas.dimensions.size / canvas.dimensions.distance)
.filter(range => range < distance)
.reduce((a, b) => Math.max(a, b), 0);
if (targetDistance) {
if (distance < targetDistance + rasterWidth) {
event.data.destination.x = origin.x + deltaX * targetDistance / distance;
event.data.destination.y = origin.y + deltaY * targetDistance / distance;
}
}
// Don't apply colors if the current user doesn't have at least observer permissions
if (this.draggedEntity.actor.permission < 2) {
// If this is a pc and alwaysShowSpeedForPCs is enabled we show the color anyway
if (!(this.draggedEntity.actor.data.type === "character" && game.settings.get(settingsKey, "alwaysShowSpeedForPCs")))
return this.color
}
const distance = startDistance + subDistance
if (!this.dragRulerRanges)
this.dragRulerRanges = getRangesFromSpeedProvider(this.draggedEntity);
const ranges = this.dragRulerRanges;
if (ranges.length === 0)
return this.color
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
}
+3 -17
View File
@@ -1,6 +1,6 @@
import {measureDistances} from "./compatibility.js";
import {recalculate, updateCombatantDragRulerFlags} from "./socket.js";
import {getTokenShape, isClose, zip} from "./util.js";
import {getTokenShape, zip} from "./util.js";
function initTrackingFlag(combatant) {
const initialFlag = {passedWaypoints: [], trackedRound: 0};
@@ -49,14 +49,14 @@ function calculateUpdate(combat, token, rays) {
}
// Add the passed waypoints to the combatant
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active;
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active && (!game.modules.get("TerrainLayer")?.active || canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS);
const dragRulerFlags = combatant.data.flags.dragRuler;
const waypoints = dragRulerFlags.passedWaypoints;
for (const ray of rays) {
// Ignore rays that have the same start and end coordinates
if (ray.A.x !== ray.B.x || ray.A.y !== ray.B.y) {
if (terrainRulerAvailable) {
measureDistances([{ray}], token, getTokenShape(token), {terrainRulerInitialState: waypoints[waypoints.length - 1]?.dragRulerFinalState, enableTerrainRuler: terrainRulerAvailable});
measureDistances([{ray}], token, getTokenShape(token), {terrainRulerInitialState: waypoints[waypoints.length - 1]?.dragRulerFinalState});
ray.A.dragRulerVisitedSpaces = ray.terrainRulerVisitedSpaces;
ray.A.dragRulerFinalState = ray.terrainRulerFinalState;
}
@@ -81,20 +81,6 @@ export function getMovementHistory(token) {
return dragRulerFlags.passedWaypoints ?? [];
}
export async function removeLastHistoryEntryIfAt(token, x, y) {
const history = getMovementHistory(token);
if (history.length === 0)
return;
const entry = history[history.length - 1];
if (!isClose(x + token.w / 2, entry.x, 0.1) || !isClose(y + token.h / 2, entry.y, 0.1)) {
return;
}
history.pop();
const combat = game.combat;
const combatant = combat.getCombatantByToken(token.id);
await updateCombatantDragRulerFlags(combat, [{_id: combatant.id, dragRulerFlags: combatant.data.flags.dragRuler}]);
}
export async function resetMovementHistory(combat, combatantId) {
const combatant = combat.combatants.get(combatantId);
const dragRulerFlags = combatant.data.flags.dragRuler;
+98
View File
@@ -0,0 +1,98 @@
import {getCenterFromGridPositionObj} from "./foundry_fixes.js";
// TODO Wipe cache if walls layer is being modified
let cached_nodes = undefined;
function get_node(pos, initialize=true) {
if (!cached_nodes)
// TODO Check if ceil is the right thing to do here
cached_nodes = new Array(Math.ceil(canvas.dimensions.sceneHeight / canvas.dimensions.size));
if (!cached_nodes[pos.y])
cached_nodes[pos.y] = new Array(Math.ceil(canvas.dimensions.sceneWidth / canvas.dimensions.size));
if (!cached_nodes[pos.y][pos.x]) {
cached_nodes[pos.y][pos.x] = {x: pos.x, y: pos.y};
}
const node = cached_nodes[pos.y][pos.x];
if (initialize && !node.edges) {
node.edges = [];
for (const neighborPos of neighbors(pos)) {
// TODO Work with pixels instead of grid locations
if (!canvas.walls.checkCollision(new Ray(getCenterFromGridPositionObj(pos), getCenterFromGridPositionObj(neighborPos)))) {
const neighbor = get_node(neighborPos, false);
node.edges.push({target: neighbor, cost: 1});
}
}
}
return node;
}
function* neighbors(pos) {
for (let y = -1;y < 2;y++) {
for (let x = -1;x < 2;x++) {
if (x != 0 || y != 0)
yield {x: pos.x + x, y: pos.y + y};
}
}
}
function calculate_path(from, to) {
const nextNodes = [{node: get_node(to), cost: 0, estimated: estimate_cost(to, from), previous: null}];
const previousNodes = new Set();
while (nextNodes.length > 0) {
// Sort by estimated cost, high to low
// TODO Re-sorting every iteration is expensive. Think of something better
nextNodes.sort((a, b) => b.estimated - a.estimated);
// Get node with cheapest estimate
const currentNode = nextNodes.pop();
if (currentNode.node.x === from.x && currentNode.node.y === from.y)
return currentNode;
previousNodes.add(currentNode.node);
for (const edge of currentNode.node.edges) {
const neighborNode = get_node(edge.target);
if (previousNodes.has(neighborNode))
continue;
// TODO We currently assume a cost of one for all transitions. Change this for 5/10/5 or difficult terrain support
// We charge an extra 0.0001 for diagonals
const isDiagonal = currentNode.node.x !== neighborNode.x && currentNode.node.y !== neighborNode.y;
const edgeCost = isDiagonal ? 1.0001 : 1;
const neighbor = {node: neighborNode, cost: currentNode.cost + edgeCost, estimated: currentNode.cost + edgeCost + estimate_cost(neighborNode, from), previous: currentNode};
const neighborIndex = nextNodes.findIndex(node => node.node === neighbor.node);
if (neighborIndex >= 0) {
// If the neighbor is cheaper to reach via the current route than through previously discovered routes, replace it
if (nextNodes[neighborIndex].cost > neighbor.cost) {
nextNodes[neighborIndex] = neighbor;
}
}
else {
nextNodes.push(neighbor);
}
}
}
}
function estimate_cost(pos, target) {
return Math.max(Math.abs(pos.x - target.x), Math.abs(pos.y - target.y));
}
export function find_path(from, to) {
const lastNode = calculate_path(from, to);
if (!lastNode)
return null;
const path = [];
let currentNode = lastNode;
while (currentNode) {
// TODO Check if the distance doesn't change
if (path.length >= 2 && !canvas.walls.checkCollision(new Ray(getCenterFromGridPositionObj(currentNode.node), getCenterFromGridPositionObj(path[path.length - 2]))))
// Replace last waypoint if the current waypoint leads to a valid path
path[path.length - 1] = {x: currentNode.node.x, y: currentNode.node.y};
else
path.push({x: currentNode.node.x, y: currentNode.node.y});
currentNode = currentNode.previous;
}
return path;
}
export function wipe_cache() {
cached_nodes = undefined;
}
+27 -55
View File
@@ -1,8 +1,7 @@
import {getColorForDistanceAndToken, getRangesFromSpeedProvider} from "./api.js";
import {cancelScheduledMeasurement, measure} from "./foundry_imports.js"
import {measure} from "./foundry_imports.js"
import {getMovementHistory} from "./movement_tracking.js";
import {settingsKey} from "./settings.js";
import {getSnapPointForEntity, setSnapParameterOnOptions} from "./util.js";
import {getSnapPointForEntity} from "./util.js";
export class DragRulerRuler extends Ruler {
// Functions below are overridden versions of functions in Ruler
@@ -17,22 +16,18 @@ export class DragRulerRuler extends Ruler {
this.previousWaypoints = [];
this.previousLabels.removeChildren().forEach(c => c.destroy());
this.dragRulerRanges = undefined;
cancelScheduledMeasurement.call(this);
}
async moveToken(event) {
// This function is invoked by left clicking
if (!this.isDragRuler)
return await super.moveToken(event);
let options = {};
setSnapParameterOnOptions(this, options);
if (!game.settings.get(settingsKey, "swapSpacebarRightClick")) {
this.dragRulerAddWaypoint(this.destination, options);
const snap = !event.shiftKey;
this.dragRulerAddWaypoint(this.destination, snap);
}
else {
this.dragRulerDeleteWaypoint(event, options);
this.dragRulerDeleteWaypoint();
}
}
@@ -40,8 +35,13 @@ export class DragRulerRuler extends Ruler {
const json = super.toJSON();
if (this.draggedEntity) {
const isToken = this.draggedEntity instanceof Token;
json["draggedEntityIsToken"] = isToken;
json["draggedEntity"] = this.draggedEntity.id;
json.draggedEntityIsToken = isToken;
json.draggedEntity = this.draggedEntity.id;
json.waypoints = json.waypoints.map(old => {
let w = duplicate(old);
w.isPathfinding = undefined;
return w;
});
}
return json;
}
@@ -57,7 +57,6 @@ export class DragRulerRuler extends Ruler {
else
this.draggedEntity = canvas.templates.get(data.draggedEntity);
}
super.update(data);
}
@@ -76,9 +75,8 @@ export class DragRulerRuler extends Ruler {
}
// The functions below aren't present in the orignal Ruler class and are added by Drag Ruler
dragRulerAddWaypoint(point, options={}) {
options.snap = options.snap ?? true;
if (options.snap) {
dragRulerAddWaypoint(point, snap=true) {
if (snap) {
point = getSnapPointForEntity(point.x, point.y, this.draggedEntity);
}
this.waypoints.push(new PIXI.Point(point.x, point.y));
@@ -98,36 +96,28 @@ export class DragRulerRuler extends Ruler {
this.labels.removeChildren().forEach(c => c.destroy());
}
dragRulerDeleteWaypoint(event={preventDefault: () => {return}}, options={}) {
options.snap = options.snap ?? true;
dragRulerDeleteWaypoint(event={preventDefault: () => {return}}) {
if (this.waypoints.filter(w => !w.isPrevious).length > 1) {
event.preventDefault();
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens);
const rulerOffset = this.rulerOffset;
// Options are not passed to _removeWaypoint in vanilla Foundry.
// Send them in case other modules have overriden that behavior and accept an options parameter (Toggle Snap to Grid)
this._removeWaypoint({x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y}, options);
this._removeWaypoint({x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y});
game.user.broadcastActivity({ruler: this});
}
else {
this.dragRulerAbortDrag(event);
const token = this.draggedEntity;
this._endMeasurement();
// Deactivate the drag workflow in mouse
token.mouseInteractionManager._deactivateDragEvents();
token.mouseInteractionManager.state = token.mouseInteractionManager.states.HOVER;
// This will cancel the current drag operation
// Pass in a fake event that hopefully is enough to allow other modules to function
token._onDragLeftCancel(event);
}
}
dragRulerAbortDrag(event={preventDefault: () => {return}}) {
const token = this.draggedEntity;
this._endMeasurement();
// Deactivate the drag workflow in mouse
token.mouseInteractionManager._deactivateDragEvents();
token.mouseInteractionManager.state = token.mouseInteractionManager.states.HOVER;
// This will cancel the current drag operation
// Pass in a fake event that hopefully is enough to allow other modules to function
token._onDragLeftCancel(event);
}
async dragRulerRecalculate(tokenIds) {
if (this._state !== Ruler.STATES.MEASURING)
return;
@@ -138,7 +128,7 @@ export class DragRulerRuler extends Ruler {
if (game.settings.get(settingsKey, "enableMovementHistory"))
this.dragRulerAddWaypointHistory(getMovementHistory(this.draggedEntity));
for (const waypoint of waypoints) {
this.dragRulerAddWaypoint(waypoint, {snap: false});
this.dragRulerAddWaypoint(waypoint, false);
}
this.measure(this.destination);
game.user.broadcastActivity({ruler: this});
@@ -153,22 +143,4 @@ export class DragRulerRuler extends Ruler {
return ray;
});
}
dragRulerGetColorForDistance(distance) {
if (!this.isDragRuler)
return this.color;
if (!this.draggedEntity.actor) {
return this.color;
}
// Don't apply colors if the current user doesn't have at least observer permissions
if (this.draggedEntity.actor.permission < 2) {
// If this is a pc and alwaysShowSpeedForPCs is enabled we show the color anyway
if (!(this.draggedEntity.actor.data.type === "character" && game.settings.get(settingsKey, "alwaysShowSpeedForPCs")))
return this.color;
}
distance = Math.round(distance * 100) / 100;
if (!this.dragRulerRanges)
this.dragRulerRanges = getRangesFromSpeedProvider(this.draggedEntity);
return getColorForDistanceAndToken(distance, this.draggedEntity, this.dragRulerRanges);
}
}
+9 -18
View File
@@ -11,15 +11,6 @@ export function registerSettings() {
default: "fresh install"
})
game.settings.register(settingsKey, "autoStartMeasurement", {
name: "drag-ruler.settings.autoStartMeasurement.name",
hint: "drag-ruler.settings.autoStartMeasurement.hint",
scope: "client",
config: true,
type: Boolean,
default: true,
});
game.settings.register(settingsKey, "swapSpacebarRightClick", {
name: "drag-ruler.settings.swapSpacebarRightClick.name",
hint: "drag-ruler.settings.swapSpacebarRightClick.hint",
@@ -29,15 +20,6 @@ export function registerSettings() {
default: false,
})
game.settings.register(settingsKey, "useGridlessRaster", {
name: "drag-ruler.settings.useGridlessRaster.name",
hint: "drag-ruler.settings.useGridlessRaster.hint",
scope: "client",
config: true,
type: Boolean,
default: true,
});
game.settings.register(settingsKey, "alwaysShowSpeedForPCs", {
name: "drag-ruler.settings.alwaysShowSpeedForPCs.name",
hint: "drag-ruler.settings.alwaysShowSpeedForPCs.hint",
@@ -65,6 +47,15 @@ export function registerSettings() {
default: true,
});
game.settings.register(settingsKey, "autoPathfinding", {
name: "drag-ruler.settings.autoPathfinding.name",
hint: "drag-ruler.settings.autoPathfinding.hint",
scpoe: "client",
config: true,
type: Boolean,
defualt: true,
});
game.settings.register(settingsKey, "lastTerrainRulerHintTime", {
config: false,
type: Number,
+3 -8
View File
@@ -61,18 +61,13 @@ export class SpeedProvider {
* The area indicates the whole area that the token will occupy (for tokens larger than 1x1) the array will more than one entry.
* The return value should be an integer indicating a multiplicator by that the cost of that step should be increased.
* (1 is regular cost, 2 costs double, 3 costs triple, ...)
*
* Parameters:
* - options: An object used to configure Enhanced Terrain Layer's cost calculation. Ex: If options.ignoreGrid is set to true, then Euclidean measurement can be forced on a gridded map.
*
* This function is only called if the Enhanced Terrain Layer and Terrain Ruler modules are enabled.
* This function is only called if the TerrainLayer and TerrainRuler modules are enabled.
*
* Implementing this method is optional and only needs to be done if you want to provide a custom cost function (for example to allow tokens to ignore difficult terrain)
*/
getCostForStep(token, area, options={}) {
getCostForStep(token, area) {
// 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));
const costs = area.map(space => terrainRuler.getCost(space.x, space.y, {token}));
// Return the maximum of the costs
return costs.reduce((max, current) => Math.max(max, current))
}
+1 -1
View File
@@ -8,7 +8,7 @@ export function getDefaultSpeedAttribute() {
case "dnd5e":
return "actor.data.data.attributes.movement.walk"
case "lancer":
return "actor.data.data.derived.speed"
return "actor.data.data.mech.speed"
case "pf1":
case "D35E":
return "actor.data.data.attributes.speed.land.total"
+16 -60
View File
@@ -10,61 +10,33 @@ export function getSnapPointForToken(x, y, token) {
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
return new PIXI.Point(x, y);
}
if (canvas.grid.isHex) {
if (game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) {
if (token.document.getFlag("hex-size-support", "borderSize") % 2 === 0) {
const snapPoint = CONFIG.hexSizeSupport.findVertexSnapPoint(x, y, token, canvas.grid.grid)
return new PIXI.Point(snapPoint.x, snapPoint.y)
}
else {
return new PIXI.Point(...canvas.grid.getCenter(x, y))
}
if (canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) {
if (token.getFlag("hex-size-support", "borderSize") % 2 === 0) {
const snapPoint = CONFIG.hexSizeSupport.findVertexSnapPoint(x, y, token, canvas.grid.grid)
return new PIXI.Point(snapPoint.x, snapPoint.y)
}
else {
return new PIXI.Point(...canvas.grid.getCenter(x, y));
return new PIXI.Point(...canvas.grid.getCenter(x, y))
}
}
const [topLeftX, topLeftY] = canvas.grid.getTopLeft(x, y);
let cellX, cellY;
if (token.data.width % 2 === 0)
cellX = x - canvas.grid.h / 2;
else
cellX = x;
if (token.data.height % 2 === 0)
cellY = y - canvas.grid.h / 2;
else
cellY = y;
const [centerX, centerY] = canvas.grid.getCenter(cellX, cellY);
let snapX, snapY;
// Tiny tokens can snap to the cells corners
if (token.data.width <= 0.5) {
if (!canvas.grid.isHex && token.data.width <= 0.5) {
const [topLeftX, topLeftY] = canvas.grid.getTopLeft(x, y);
const offsetX = x - topLeftX;
const offsetY = y - topLeftY;
const subGridWidth = Math.floor(canvas.grid.w / 2);
const subGridHeight = Math.floor(canvas.grid.h / 2);
const subGridPosX = Math.floor(offsetX / subGridWidth);
snapX = topLeftX + (subGridPosX + 0.5) * subGridWidth;
const subGridPosY = Math.floor(offsetY / subGridHeight);
return new PIXI.Point(topLeftX + (subGridPosX + 0.5) * subGridWidth, topLeftY + (subGridPosY + 0.5) * subGridHeight);
}
// Tokens with odd multipliers (1x1, 3x3, ...) and tokens smaller than 1x1 but bigger than 0.5x0.5 snap to the center of the grid cell
else if (Math.round(token.data.width) % 2 === 1 || token.data.width < 1) {
snapX = centerX;
// Hex tokens, tokens with odd multipliers (1x1, 3x3, ...) and tokens smaller than 1x1 but bigger than 0.5x0.5 snap to the center of the grid cell
if (canvas.grid.isHex || Math.round(token.data.width) % 2 === 1 || token.data.width < 1) {
return new PIXI.Point(...canvas.grid.getCenter(x, y))
}
// All remaining tokens (those with even or fractional multipliers on square grids) snap to the intersection points of the grid
else {
snapX = centerX + canvas.grid.w / 2;
}
if (token.data.height <= 0.5) {
const offsetY = y - topLeftY;
const subGridHeight = Math.floor(canvas.grid.h / 2);
const subGridPosY = Math.floor(offsetY / subGridHeight);
snapY = topLeftY + (subGridPosY + 0.5) * subGridHeight;
}
else if (Math.round(token.data.height) % 2 === 1 || token.data.height < 1) {
snapY = centerY;
}
else {
snapY = centerY + canvas.grid.h / 2;
}
return new PIXI.Point(snapX, snapY);
const [snappedX, snappedY] = canvas.grid.getCenter(x - canvas.grid.w / 2, y - canvas.grid.h / 2)
return new PIXI.Point(snappedX + canvas.grid.w / 2, snappedY + canvas.grid.h / 2)
}
export function getSnapPointForMeasuredTemplate(x, y) {
@@ -226,19 +198,3 @@ export function applyTokenSizeOffset(waypoints, token) {
return waypoints.map(w => new PIXI.Point(w.x + waypointOffset.x, w.y + waypointOffset.y))
}
export function setSnapParameterOnOptions(sourceObject, options) {
// Allow outside modules to override snapping
if (sourceObject.snapOverride?.active) {
options.snapOverrideActive = true;
options.snap = sourceObject.snapOverride.snap;
sourceObject.snapOverride = undefined; // remove it to prevent any lingering data issues
}
else {
options.snap = !game.keyboard._downKeys.has("Shift");
}
}
export function isClose(a, b, delta) {
return Math.abs(a - b) <= delta;
}