Merge branch 'develop' into pathfinding-difficult-terrain
This commit is contained in:
@@ -1,3 +1,14 @@
|
|||||||
|
## 1.12.3
|
||||||
|
### Bugfixes
|
||||||
|
- Fixed a bug that could cause foundry to freeze indefinitely when trying to pathfind to an unreachalbe location (thanks to JDCalvert)
|
||||||
|
- Fixed a bug that caused the pathfinder to route through one-directional walls from the wrong direction (thanks to JDCalvert)
|
||||||
|
- Fixed a bug that could cause Drag Ruler to write errors into the JS console during regular usage
|
||||||
|
|
||||||
|
### Compatibility
|
||||||
|
- Drag Ruler's generic speed provider is now aware of good defaults for DnD 4th Edition
|
||||||
|
- Drag Ruler's pathfinder should now be compatible with the Wall Height and Levels modules (thanks to JDCalvert)
|
||||||
|
|
||||||
|
|
||||||
## 1.12.2
|
## 1.12.2
|
||||||
### Bugfixes
|
### Bugfixes
|
||||||
- Fixed a bug where the pathfinder on gridless scenes sometimes wasn't able to find a way around corners with specific angles
|
- Fixed a bug where the pathfinder on gridless scenes sometimes wasn't able to find a way around corners with specific angles
|
||||||
|
|||||||
+82
-39
@@ -9,9 +9,59 @@ import {PriorityQueueSet} from "./data_structures.js";
|
|||||||
import { buildCostFunction } from "./api.js";
|
import { buildCostFunction } from "./api.js";
|
||||||
import { measure } from "./foundry_imports.js";
|
import { measure } from "./foundry_imports.js";
|
||||||
|
|
||||||
// TODO Account for changing terrain per token in the caches
|
class Cache {
|
||||||
let caches = new Map();
|
static maxCacheIds = 5;
|
||||||
let cacheElevation;
|
|
||||||
|
constructor() {
|
||||||
|
this.nodes = new Map();
|
||||||
|
this.lastUsed = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.nodes.clear();
|
||||||
|
this.lastUsed.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cache associated with the given cache ID, creating a new one
|
||||||
|
* if we don't already have one
|
||||||
|
*/
|
||||||
|
getCachedNodes(cacheId) {
|
||||||
|
// Track that we've last used this cache right now
|
||||||
|
this.lastUsed.set(cacheId, Date.now());
|
||||||
|
|
||||||
|
// Get the nodes for the cacheId. If we don't already have one, create one
|
||||||
|
let cachedNodes = this.nodes.get(cacheId);
|
||||||
|
if (!cachedNodes) {
|
||||||
|
cachedNodes = new Array(gridHeight);
|
||||||
|
for (let y = 0; y < gridHeight; y++) {
|
||||||
|
cachedNodes[y] = new Array(gridWidth);
|
||||||
|
for (let x = 0; x < gridWidth; x++) {
|
||||||
|
cachedNodes[y][x] = {x, y};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.nodes.set(cacheId, cachedNodes);
|
||||||
|
|
||||||
|
// Since we're adding a new cache, check if we have too many and,
|
||||||
|
// if we do, get rid of the one that was last used longest ago
|
||||||
|
if (this.lastUsed.size > Cache.maxCacheIds) {
|
||||||
|
let oldest;
|
||||||
|
for (let entry of this.lastUsed) {
|
||||||
|
if (!oldest || oldest[1] > entry[1]) {
|
||||||
|
oldest = entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.nodes.delete(oldest[0]);
|
||||||
|
this.lastUsed.delete(oldest[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedNodes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache = new Cache();
|
||||||
|
|
||||||
let use5105 = false;
|
let use5105 = false;
|
||||||
let gridlessPathfinders = new Map();
|
let gridlessPathfinders = new Map();
|
||||||
let gridWidth, gridHeight;
|
let gridWidth, gridHeight;
|
||||||
@@ -27,8 +77,6 @@ export function isPathfindingEnabled() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function findPath(from, to, token, previousWaypoints) {
|
export function findPath(from, to, token, previousWaypoints) {
|
||||||
checkCacheValid(token);
|
|
||||||
|
|
||||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||||
let tokenSize = Math.max(token.data.width, token.data.height) * canvas.dimensions.size;
|
let tokenSize = Math.max(token.data.width, token.data.height) * canvas.dimensions.size;
|
||||||
let pathfinder = gridlessPathfinders.get(tokenSize);
|
let pathfinder = gridlessPathfinders.get(tokenSize);
|
||||||
@@ -39,14 +87,15 @@ export function findPath(from, to, token, previousWaypoints) {
|
|||||||
paintGridlessPathfindingDebug(pathfinder);
|
paintGridlessPathfindingDebug(pathfinder);
|
||||||
return GridlessPathfinding.findPath(pathfinder, from, to);
|
return GridlessPathfinding.findPath(pathfinder, from, to);
|
||||||
} else {
|
} else {
|
||||||
const lastNode = calculatePath(from, to, token, previousWaypoints);
|
const cachedNodes = getCachedNodes(token);
|
||||||
|
const lastNode = calculatePath(from, to, cachedNodes, token, previousWaypoints);
|
||||||
if (!lastNode)
|
if (!lastNode)
|
||||||
return null;
|
return null;
|
||||||
paintGriddedPathfindingDebug(lastNode, token);
|
paintGriddedPathfindingDebug(lastNode, token);
|
||||||
const path = [];
|
const path = [];
|
||||||
let currentNode = lastNode;
|
let currentNode = lastNode;
|
||||||
while (currentNode) {
|
while (currentNode) {
|
||||||
if (path.length >= 2 && !stepCollidesWithWall(path[path.length - 2], currentNode.node, token))
|
if (path.length >= 2 && !stepCollidesWithWall(path[path.length - 2], currentNode.node, token)) {
|
||||||
// Replace last waypoint if the current waypoint leads to a valid path that isn't longer than the old path
|
// Replace last waypoint if the current waypoint leads to a valid path that isn't longer than the old path
|
||||||
if (window.terrainRuler) {
|
if (window.terrainRuler) {
|
||||||
let startNode = getCenterFromGridPositionObj(path[path.length - 2]);
|
let startNode = getCenterFromGridPositionObj(path[path.length - 2]);
|
||||||
@@ -83,24 +132,32 @@ export function findPath(from, to, token, previousWaypoints) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNode(pos, token, initialize=true) {
|
/**
|
||||||
let shapeId = getTokenShapeId(token);
|
* Build a cache ID based on the current token's data and then retrieve the cache to use from that
|
||||||
let cache = caches.get(shapeId);
|
*/
|
||||||
if (!cache) {
|
function getCachedNodes(token) {
|
||||||
cache = new Array(gridHeight);
|
const cacheData = {};
|
||||||
caches.set(shapeId, cache);
|
|
||||||
}
|
// Different-sized tokens snap to different points on the grid,
|
||||||
if (!cache[pos.y])
|
// so they might follow a different path to other tokens
|
||||||
cache[pos.y] = new Array(gridWidth);
|
cacheData.tokenShape = getTokenShapeId(token);
|
||||||
if (!cache[pos.y][pos.x]) {
|
|
||||||
cache[pos.y][pos.x] = pos;
|
// If levels is enabled, the token's elevation can affect which walls
|
||||||
|
// they need to worry about
|
||||||
|
if (game.modules.get("levels")?.active) {
|
||||||
|
cacheData.elevation = token.data.elevation;
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = cache[pos.y][pos.x];
|
const cacheId = JSON.stringify(cacheData);
|
||||||
|
return cache.getCachedNodes(cacheId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNode(pos, cachedNodes, token, initialize = true) {
|
||||||
|
const node = cachedNodes[pos.y][pos.x];
|
||||||
if (initialize && !node.edges) {
|
if (initialize && !node.edges) {
|
||||||
node.edges = [];
|
node.edges = [];
|
||||||
for (const neighborPos of canvas.grid.grid.getNeighbors(pos.y, pos.x).map(([y, x]) => {return {x, y};})) {
|
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) {
|
if (neighborPos.x < 0 || neighborPos.y < 0 || neighborPos.x >= gridWidth || neighborPos.y >= gridHeight) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +182,7 @@ function getNode(pos, token, initialize=true) {
|
|||||||
// TODO Account for difficult terrain
|
// TODO Account for difficult terrain
|
||||||
edgeCost = isDiagonal ? (use5105 ? 1.5 : 1.0001) : 1;
|
edgeCost = isDiagonal ? (use5105 ? 1.5 : 1.0001) : 1;
|
||||||
}
|
}
|
||||||
const neighbor = getNode(neighborPos, token, false);
|
const neighbor = getNode(neighborPos, cachedNodes, token, false);
|
||||||
node.edges.push({target: neighbor, cost: edgeCost});
|
node.edges.push({target: neighbor, cost: edgeCost});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,7 +190,7 @@ function getNode(pos, token, initialize=true) {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculatePath(from, to, token, previousWaypoints) {
|
function calculatePath(from, to, cachedNodes, token, previousWaypoints) {
|
||||||
use5105 = game.system.id === "pf2e" || canvas.grid.diagonalRule === "5105";
|
use5105 = game.system.id === "pf2e" || canvas.grid.diagonalRule === "5105";
|
||||||
let startCost = 0;
|
let startCost = 0;
|
||||||
if (use5105 && canvas.grid.type === CONST.GRID_TYPES.SQUARE) {
|
if (use5105 && canvas.grid.type === CONST.GRID_TYPES.SQUARE) {
|
||||||
@@ -146,7 +203,7 @@ function calculatePath(from, to, token, previousWaypoints) {
|
|||||||
|
|
||||||
nextNodes.pushWithPriority(
|
nextNodes.pushWithPriority(
|
||||||
{
|
{
|
||||||
node: getNode(to, token),
|
node: getNode(to, cachedNodes, token),
|
||||||
cost: startCost,
|
cost: startCost,
|
||||||
estimated: startCost + estimateCost(to, from),
|
estimated: startCost + estimateCost(to, from),
|
||||||
previous: null
|
previous: null
|
||||||
@@ -161,7 +218,7 @@ function calculatePath(from, to, token, previousWaypoints) {
|
|||||||
}
|
}
|
||||||
previousNodes.add(currentNode.node);
|
previousNodes.add(currentNode.node);
|
||||||
for (const edge of currentNode.node.edges) {
|
for (const edge of currentNode.node.edges) {
|
||||||
const neighborNode = getNode(edge.target, token);
|
const neighborNode = getNode(edge.target, cachedNodes, token);
|
||||||
if (previousNodes.has(neighborNode)) {
|
if (previousNodes.has(neighborNode)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -202,7 +259,7 @@ function stepCollidesWithWall(from, to, token) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function wipePathfindingCache() {
|
export function wipePathfindingCache() {
|
||||||
caches.clear();
|
cache.clear();
|
||||||
for (const pathfinder of gridlessPathfinders.values()) {
|
for (const pathfinder of gridlessPathfinders.values()) {
|
||||||
GridlessPathfinding.free(pathfinder);
|
GridlessPathfinding.free(pathfinder);
|
||||||
}
|
}
|
||||||
@@ -211,20 +268,6 @@ export function wipePathfindingCache() {
|
|||||||
debugGraphics.removeChildren().forEach(c => c.destroy());
|
debugGraphics.removeChildren().forEach(c => c.destroy());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the current cache is still suitable for the path we're about to find. If not, clear the cache
|
|
||||||
*/
|
|
||||||
function checkCacheValid(token) {
|
|
||||||
// If levels is enabled, the cache is invalid if it was made for a
|
|
||||||
if (game.modules.get("levels")?.active) {
|
|
||||||
const tokenElevation = token.data.elevation;
|
|
||||||
if (tokenElevation !== cacheElevation) {
|
|
||||||
cacheElevation = tokenElevation;
|
|
||||||
wipePathfindingCache();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initializePathfinding() {
|
export function initializePathfinding() {
|
||||||
gridWidth = Math.ceil(canvas.dimensions.width / canvas.grid.w);
|
gridWidth = Math.ceil(canvas.dimensions.width / canvas.grid.w);
|
||||||
gridHeight = Math.ceil(canvas.dimensions.height / canvas.grid.h);
|
gridHeight = Math.ceil(canvas.dimensions.height / canvas.grid.h);
|
||||||
|
|||||||
+3
-1
@@ -39,5 +39,7 @@ export function recalculate(tokens) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _socketRecalculate(tokenIds) {
|
function _socketRecalculate(tokenIds) {
|
||||||
return canvas.controls.ruler.dragRulerRecalculate(tokenIds);
|
const ruler = canvas.controls.ruler;
|
||||||
|
if (ruler.isDragRuler)
|
||||||
|
ruler.dragRulerRecalculate(tokenIds);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
"name": "drag-ruler",
|
"name": "drag-ruler",
|
||||||
"title": "Drag Ruler",
|
"title": "Drag Ruler",
|
||||||
"description": "When dragging a token displays a ruler showing how far you've moved that token.",
|
"description": "When dragging a token displays a ruler showing how far you've moved that token.",
|
||||||
"version": "1.12.2",
|
"version": "1.12.3",
|
||||||
"minimumCoreVersion" : "9.245",
|
"minimumCoreVersion" : "9.245",
|
||||||
"compatibleCoreVersion" : "9",
|
"compatibleCoreVersion" : "9",
|
||||||
"authors": [
|
"authors": [
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
],
|
],
|
||||||
"socket": true,
|
"socket": true,
|
||||||
"url": "https://github.com/manuelVo/foundryvtt-drag-ruler",
|
"url": "https://github.com/manuelVo/foundryvtt-drag-ruler",
|
||||||
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/releases/download/v1.12.2/drag-ruler-1.12.2.zip",
|
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/releases/download/v1.12.3/drag-ruler-1.12.3.zip",
|
||||||
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/master/module.json",
|
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/master/module.json",
|
||||||
"readme": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/README.md",
|
"readme": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/README.md",
|
||||||
"changelog": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/CHANGELOG.md",
|
"changelog": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/CHANGELOG.md",
|
||||||
|
|||||||
Generated
+1
-1
@@ -26,7 +26,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridless-pathfinding"
|
name = "gridless-pathfinding"
|
||||||
version = "1.12.2"
|
version = "1.12.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gridless-pathfinding"
|
name = "gridless-pathfinding"
|
||||||
version = "1.12.2"
|
version = "1.12.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|||||||
Reference in New Issue
Block a user