Merge branch 'develop' into pathfinding-difficult-terrain

This commit is contained in:
Manuel Vögele
2022-03-08 11:10:34 +01:00
6 changed files with 101 additions and 45 deletions
+11
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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",
+1 -1
View File
@@ -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
View File
@@ -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