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
+83 -40
View File
@@ -9,9 +9,59 @@ import {PriorityQueueSet} from "./data_structures.js";
import { buildCostFunction } from "./api.js";
import { measure } from "./foundry_imports.js";
// TODO Account for changing terrain per token in the caches
let caches = new Map();
let cacheElevation;
class Cache {
static maxCacheIds = 5;
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 gridlessPathfinders = new Map();
let gridWidth, gridHeight;
@@ -27,8 +77,6 @@ export function isPathfindingEnabled() {
}
export function findPath(from, to, token, previousWaypoints) {
checkCacheValid(token);
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
let tokenSize = Math.max(token.data.width, token.data.height) * canvas.dimensions.size;
let pathfinder = gridlessPathfinders.get(tokenSize);
@@ -39,15 +87,16 @@ export function findPath(from, to, token, previousWaypoints) {
paintGridlessPathfindingDebug(pathfinder);
return GridlessPathfinding.findPath(pathfinder, from, to);
} else {
const lastNode = calculatePath(from, to, token, previousWaypoints);
const cachedNodes = getCachedNodes(token);
const lastNode = calculatePath(from, to, cachedNodes, token, previousWaypoints);
if (!lastNode)
return null;
paintGriddedPathfindingDebug(lastNode, token);
const path = [];
let currentNode = lastNode;
while (currentNode) {
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
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
if (window.terrainRuler) {
let startNode = getCenterFromGridPositionObj(path[path.length - 2]);
let middleNode = getCenterFromGridPositionObj(path[path.length - 1]);
@@ -83,24 +132,32 @@ export function findPath(from, to, token, previousWaypoints) {
}
}
function getNode(pos, token, initialize=true) {
let shapeId = getTokenShapeId(token);
let cache = caches.get(shapeId);
if (!cache) {
cache = new Array(gridHeight);
caches.set(shapeId, cache);
}
if (!cache[pos.y])
cache[pos.y] = new Array(gridWidth);
if (!cache[pos.y][pos.x]) {
cache[pos.y][pos.x] = pos;
/**
* Build a cache ID based on the current token's data and then retrieve the cache to use from that
*/
function getCachedNodes(token) {
const cacheData = {};
// Different-sized tokens snap to different points on the grid,
// so they might follow a different path to other tokens
cacheData.tokenShape = getTokenShapeId(token);
// 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) {
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) {
if (neighborPos.x < 0 || neighborPos.y < 0 || neighborPos.x >= gridWidth || neighborPos.y >= gridHeight) {
continue;
}
@@ -125,7 +182,7 @@ function getNode(pos, token, initialize=true) {
// TODO Account for difficult terrain
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});
}
}
@@ -133,7 +190,7 @@ function getNode(pos, token, initialize=true) {
return node;
}
function calculatePath(from, to, token, previousWaypoints) {
function calculatePath(from, to, cachedNodes, token, previousWaypoints) {
use5105 = game.system.id === "pf2e" || canvas.grid.diagonalRule === "5105";
let startCost = 0;
if (use5105 && canvas.grid.type === CONST.GRID_TYPES.SQUARE) {
@@ -146,7 +203,7 @@ function calculatePath(from, to, token, previousWaypoints) {
nextNodes.pushWithPriority(
{
node: getNode(to, token),
node: getNode(to, cachedNodes, token),
cost: startCost,
estimated: startCost + estimateCost(to, from),
previous: null
@@ -161,7 +218,7 @@ function calculatePath(from, to, token, previousWaypoints) {
}
previousNodes.add(currentNode.node);
for (const edge of currentNode.node.edges) {
const neighborNode = getNode(edge.target, token);
const neighborNode = getNode(edge.target, cachedNodes, token);
if (previousNodes.has(neighborNode)) {
continue;
}
@@ -202,7 +259,7 @@ function stepCollidesWithWall(from, to, token) {
}
export function wipePathfindingCache() {
caches.clear();
cache.clear();
for (const pathfinder of gridlessPathfinders.values()) {
GridlessPathfinding.free(pathfinder);
}
@@ -211,20 +268,6 @@ export function wipePathfindingCache() {
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() {
gridWidth = Math.ceil(canvas.dimensions.width / canvas.grid.w);
gridHeight = Math.ceil(canvas.dimensions.height / canvas.grid.h);