Add difficult terrain support for pathfinding on gridded scenes
This commit is contained in:
@@ -2,7 +2,7 @@ import {measureDistances} from "./compatibility.js";
|
||||
import {getMovementHistory} from "./movement_tracking.js";
|
||||
import {GenericSpeedProvider, SpeedProvider} from "./speed_provider.js"
|
||||
import {settingsKey} from "./settings.js"
|
||||
import {getTokenShape} from "./util.js";
|
||||
import {getAreaFromPositionAndShape, getTokenShape} from "./util.js";
|
||||
|
||||
export const availableSpeedProviders = {}
|
||||
export let currentSpeedProvider = undefined
|
||||
@@ -140,6 +140,10 @@ export function getMovedDistanceFromToken(token) {
|
||||
return distances.reduce((acc, val) => acc + val, 0);
|
||||
}
|
||||
|
||||
export function buildCostFunction(token, shape) {
|
||||
return (x, y, costOptions={}) => getCostFromSpeedProvider(token, getAreaFromPositionAndShape({x, y}, shape), costOptions);
|
||||
}
|
||||
|
||||
export function registerModule(moduleId, speedProvider) {
|
||||
// Check if a module with the given id exists and is currently enabled
|
||||
const module = game.modules.get(moduleId)
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import {getCostFromSpeedProvider} from "./api.js";
|
||||
import {buildCostFunction} from "./api.js";
|
||||
import {settingsKey} from "./settings.js";
|
||||
import {getAreaFromPositionAndShape, highlightTokenShape} from "./util.js";
|
||||
import {highlightTokenShape} from "./util.js";
|
||||
|
||||
export function getHexSizeSupportTokenGridCenter(token) {
|
||||
const tokenCenterOffset = CONFIG.hexSizeSupport.getCenterOffset(token)
|
||||
@@ -27,7 +27,7 @@ export function measureDistances(segments, entity, shape, options={}) {
|
||||
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 = buildCostFunction(entity, shape);
|
||||
if (previousSegments.length > 0)
|
||||
opts.terrainRulerInitialState = previousSegments[previousSegments.length - 1].ray.dragRulerFinalState;
|
||||
return distances.concat(terrainRuler.measureDistances(newSegments, opts));
|
||||
|
||||
+62
-12
@@ -1,11 +1,12 @@
|
||||
import {getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
|
||||
import {getCenterFromGridPositionObj, getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
|
||||
import {moveWithoutAnimation, togglePathfinding} from "./keybindings.js";
|
||||
import {debugGraphics} from "./main.js";
|
||||
import {settingsKey} from "./settings.js";
|
||||
import {buildSnapPointTokenData, getSnapPointForTokenDataObj, isModuleActive, iterPairs} from "./util.js";
|
||||
import {buildSnapPointTokenData, getSnapPointForTokenDataObj, getTokenShape, getTokenShapeForTokenData, isModuleActive, iterPairs} from "./util.js";
|
||||
|
||||
import * as GridlessPathfinding from "../wasm/gridless_pathfinding.js";
|
||||
import {PriorityQueueSet, ProcessOnceQueue} from "./data_structures.js";
|
||||
import {buildCostFunction} from "./api.js";
|
||||
|
||||
class CacheLayer {
|
||||
constructor(tokenData, cacheId) {
|
||||
@@ -79,7 +80,18 @@ class Cache {
|
||||
*/
|
||||
getCacheLayer(token) {
|
||||
const tokenData = buildTokenData(token);
|
||||
const cacheId = JSON.stringify(tokenData);
|
||||
// TODO Request this from the speed providers so they can set their own options
|
||||
let terrainData = canvas.terrain.listAllTerrain({token});
|
||||
terrainData = terrainData.map(data => {
|
||||
return {
|
||||
x: data.object.x,
|
||||
y: data.object.y,
|
||||
cost: data.cost,
|
||||
shape: data.shape,
|
||||
};
|
||||
});
|
||||
const cacheIdData = {tokenData, terrainData};
|
||||
const cacheId = GridlessPathfinding.sha1(JSON.stringify(cacheIdData));
|
||||
let cacheLayer = this.layers.get(cacheId);
|
||||
// If we don't already have a cache layer for this cache ID, create one now
|
||||
if (!cacheLayer) {
|
||||
@@ -255,13 +267,37 @@ export function findPath(from, to, token, previousWaypoints) {
|
||||
const path = [];
|
||||
let currentNode = firstNode;
|
||||
while (currentNode) {
|
||||
// TODO Check if the distance doesn't change
|
||||
if (path.length >= 2 && !stepCollidesWithWall(path[path.length - 2], currentNode.node, cacheLayer.tokenData)) {
|
||||
// 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});
|
||||
// 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]);
|
||||
let endNode = getCenterFromGridPositionObj(currentNode.node);
|
||||
let oldPath = [{ray: new Ray(startNode, middleNode)}, {ray: new Ray(middleNode, endNode)}];
|
||||
let newPath = [{ray: new Ray(startNode, endNode)}];
|
||||
let costFunction = buildCostFunction(token, getTokenShape(token));
|
||||
// TODO Cache the used measurement for use in the next loop to improve performance
|
||||
let oldDistance = terrainRuler.measureDistances(oldPath, {costFunction}).reduce((a, b) => a + b);
|
||||
let newDistance = terrainRuler.measureDistances(newPath, {costFunction})[0];
|
||||
|
||||
// TODO We might need to check if the diagonal count has increased on 5-10-5
|
||||
if (newDistance < oldDistance) {
|
||||
path.pop();
|
||||
}
|
||||
else if (newDistance === oldDistance) {
|
||||
let oldNoDiagonals = oldPath[1].ray.terrainRulerFinalState?.noDiagonals;
|
||||
let newNoDiagonals = newPath[0].ray.terrainRulerFinalState?.noDiagonals;
|
||||
// This uses === && < instead of <= because the variables might be undefined (which shall lead to a true result)
|
||||
if (oldNoDiagonals === newNoDiagonals || newNoDiagonals < oldNoDiagonals) {
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
path.push({x: currentNode.node.x, y: currentNode.node.y});
|
||||
currentNode = currentNode.next;
|
||||
}
|
||||
return path;
|
||||
@@ -295,11 +331,25 @@ function getNode(pos, cacheLayer, initialize = true) {
|
||||
// TODO Work with pixels instead of grid locations
|
||||
if (!stepCollidesWithWall(pos, neighborPos, cacheLayer.tokenData)) {
|
||||
const isDiagonal = node.x !== neighborPos.x && node.y !== neighborPos.y && canvas.grid.type === CONST.GRID_TYPES.SQUARE;
|
||||
let edgeCost;
|
||||
if (window.terrainRuler) {
|
||||
let ray = new Ray(getCenterFromGridPositionObj(pos), getCenterFromGridPositionObj(neighborPos));
|
||||
let measuredDistance = terrainRuler.measureDistances([{ray}], {costFunction: buildCostFunction(cacheLayer.tokenData, getTokenShapeForTokenData(cacheLayer.tokenData))})[0];
|
||||
edgeCost = Math.round(measuredDistance / canvas.dimensions.distance);
|
||||
if (ray.terrainRulerFinalState?.noDiagonals === 1) {
|
||||
edgeCost = 1.5;
|
||||
}
|
||||
// Charge 1.0001 instead of 1 for diagonals to discourage unnecessary diagonals
|
||||
if (isDiagonal && edgeCost == 1) {
|
||||
edgeCost = 1.0001;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Count 5-10-5 diagonals as 1.5 (so two add up to 3) and 5-5-5 diagonals as 1.0001 (to discourage unnecessary diagonals)
|
||||
// TODO Account for difficult terrain
|
||||
edgeCost = isDiagonal ? (use5105 ? 1.5 : 1.0001) : 1;
|
||||
}
|
||||
const neighbor = getNode(neighborPos, cacheLayer, false);
|
||||
|
||||
// Count 5-10-5 diagonals as 1.5 (so two add up to 3) and 5-5-5 diagonals as 1.0001 (to discourage unnecessary diagonals)
|
||||
// TODO Account for difficult terrain
|
||||
let edgeCost = isDiagonal ? (use5105 ? 1.5 : 1.0001) : 1;
|
||||
node.edges.push({target: neighbor, cost: edgeCost});
|
||||
}
|
||||
}
|
||||
|
||||
+13
-9
@@ -167,15 +167,19 @@ export function getAreaFromPositionAndShape(position, shape) {
|
||||
}
|
||||
|
||||
export function getTokenShape(token) {
|
||||
if (token.scene.data.gridType === CONST.GRID_TYPES.GRIDLESS) {
|
||||
return getTokenShapeForTokenData(buildSnapPointTokenData(token), token.scene);
|
||||
}
|
||||
|
||||
export function getTokenShapeForTokenData(tokenData, scene=canvas.scene) {
|
||||
if (scene.data.gridType === CONST.GRID_TYPES.GRIDLESS) {
|
||||
return [{x: 0, y: 0}]
|
||||
}
|
||||
else if (token.scene.data.gridType === CONST.GRID_TYPES.SQUARE) {
|
||||
const topOffset = -Math.floor(token.data.height / 2)
|
||||
const leftOffset = -Math.floor(token.data.width / 2)
|
||||
else if (scene.data.gridType === CONST.GRID_TYPES.SQUARE) {
|
||||
const topOffset = -Math.floor(tokenData.height / 2)
|
||||
const leftOffset = -Math.floor(tokenData.width / 2)
|
||||
const shape = []
|
||||
for (let y = 0;y < token.data.height;y++) {
|
||||
for (let x = 0;x < token.data.width;x++) {
|
||||
for (let y = 0;y < tokenData.height;y++) {
|
||||
for (let x = 0;x < tokenData.width;x++) {
|
||||
shape.push({x: x + leftOffset, y: y + topOffset})
|
||||
}
|
||||
}
|
||||
@@ -183,8 +187,8 @@ export function getTokenShape(token) {
|
||||
}
|
||||
else {
|
||||
// Hex grids
|
||||
if (game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) {
|
||||
const borderSize = token.data.flags["hex-size-support"].borderSize;
|
||||
if (game.modules.get("hex-size-support")?.active && tokenData.hexSizeSupport.altSnappingFlag) {
|
||||
const borderSize = tokenData.hexSizeSupport.borderSize;
|
||||
let shape = [{x: 0, y: 0}];
|
||||
if (borderSize >= 2)
|
||||
shape = shape.concat([{x: 0, y: -1}, {x: -1, y: -1}]);
|
||||
@@ -193,7 +197,7 @@ export function getTokenShape(token) {
|
||||
if (borderSize >= 4)
|
||||
shape = shape.concat([{x: -2, y: -1}, {x: 1, y: -1}, {x: -1, y: -2}, {x: 0, y: -2}, {x: 1, y: -2}])
|
||||
|
||||
if (Boolean(CONFIG.hexSizeSupport.getAltOrientationFlag(token)) !== canvas.grid.grid.options.columns)
|
||||
if (Boolean(tokenData.hexSizeSupport.altOrientationFlag) !== canvas.grid.grid.options.columns)
|
||||
shape.forEach(space => space.y *= -1);
|
||||
if (canvas.grid.grid.options.columns)
|
||||
shape = shape.map(space => {return {x: space.y, y: space.x}});
|
||||
|
||||
Reference in New Issue
Block a user