Compare commits
6 Commits
v1.10.0
...
prerelease
| Author | SHA1 | Date | |
|---|---|---|---|
| 0588d3ef8b | |||
| b084d25cb6 | |||
| 654847563f | |||
| baca42a2f8 | |||
| 68ee098e61 | |||
| 3300007f93 |
@@ -34,6 +34,10 @@
|
||||
"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."
|
||||
},
|
||||
"autoPathfinding": {
|
||||
"name": "Pathfinding by default",
|
||||
"hint": "If enabled, dragging a token will automatically use a pathfinding ruler"
|
||||
},
|
||||
"enableMovementHistory": {
|
||||
"name": "Enable movement history during combat",
|
||||
"hint": "If enabled, Drag Ruler will remember the path a token took during it's turn in combat and will display it when you pick the token back up."
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
"name": "drag-ruler",
|
||||
"title": "Drag Ruler",
|
||||
"description": "When dragging a token displays a ruler showing how far you've moved that token.",
|
||||
"version": "1.7.7",
|
||||
"version": "1.7.7-prerelease1",
|
||||
"minimumCoreVersion" : "0.8.5",
|
||||
"compatibleCoreVersion" : "0.8.8",
|
||||
"authors": [
|
||||
@@ -59,7 +59,7 @@
|
||||
],
|
||||
"socket": true,
|
||||
"url": "https://github.com/manuelVo/foundryvtt-drag-ruler",
|
||||
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/v1.7.7.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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+24
-3
@@ -1,8 +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";
|
||||
|
||||
@@ -77,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];
|
||||
@@ -101,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};
|
||||
}
|
||||
@@ -116,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
|
||||
@@ -131,7 +136,7 @@ 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;
|
||||
window.clearTimeout(this.deferredMeasurementTimeout);
|
||||
@@ -148,7 +153,7 @@ function scheduleMeasurement(destination, event) {
|
||||
}
|
||||
|
||||
// This is a modified version of Ruler.measure form foundry 0.7.9
|
||||
export function measure(destination, {gridSpaces=true, snap=false} = {}) {
|
||||
export function measure(destination, {gridSpaces=true, snap=false, pathfinding=false} = {}) {
|
||||
const isToken = this.draggedEntity instanceof Token;
|
||||
if (isToken && !this.draggedEntity.isVisible)
|
||||
return []
|
||||
@@ -157,6 +162,22 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) {
|
||||
destination = getSnapPointForEntity(destination.x, destination.y, this.draggedEntity);
|
||||
}
|
||||
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
@@ -9,6 +9,7 @@ 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 { wipe_cache } from "./pathfinding.js";
|
||||
|
||||
Hooks.once("init", () => {
|
||||
registerSettings()
|
||||
@@ -137,6 +138,7 @@ function onEntityLeftDragStart(event) {
|
||||
else
|
||||
entityCenter = this.center;
|
||||
ruler.clear();
|
||||
wipe_cache();
|
||||
ruler._state = Ruler.STATES.STARTING;
|
||||
ruler.rulerOffset = {x: entityCenter.x - event.data.origin.x, y: entityCenter.y - event.data.origin.y};
|
||||
if (isToken && game.settings.get(settingsKey, "enableMovementHistory"))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+7
-2
@@ -35,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;
|
||||
}
|
||||
|
||||
@@ -47,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,
|
||||
|
||||
Reference in New Issue
Block a user