Add difficult terrain support for pathfinding on gridded scenes

This commit is contained in:
Manuel Vögele
2022-05-10 08:39:19 +02:00
parent 5904efbdd5
commit 445c03d29a
7 changed files with 171 additions and 25 deletions
+5 -1
View File
@@ -2,7 +2,7 @@ import {measureDistances} from "./compatibility.js";
import {getMovementHistory} from "./movement_tracking.js"; import {getMovementHistory} from "./movement_tracking.js";
import {GenericSpeedProvider, SpeedProvider} from "./speed_provider.js" import {GenericSpeedProvider, SpeedProvider} from "./speed_provider.js"
import {settingsKey} from "./settings.js" import {settingsKey} from "./settings.js"
import {getTokenShape} from "./util.js"; import {getAreaFromPositionAndShape, getTokenShape} from "./util.js";
export const availableSpeedProviders = {} export const availableSpeedProviders = {}
export let currentSpeedProvider = undefined export let currentSpeedProvider = undefined
@@ -140,6 +140,10 @@ export function getMovedDistanceFromToken(token) {
return distances.reduce((acc, val) => acc + val, 0); 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) { export function registerModule(moduleId, speedProvider) {
// Check if a module with the given id exists and is currently enabled // Check if a module with the given id exists and is currently enabled
const module = game.modules.get(moduleId) const module = game.modules.get(moduleId)
+3 -3
View File
@@ -1,6 +1,6 @@
import {getCostFromSpeedProvider} from "./api.js"; import {buildCostFunction} from "./api.js";
import {settingsKey} from "./settings.js"; import {settingsKey} from "./settings.js";
import {getAreaFromPositionAndShape, highlightTokenShape} from "./util.js"; import {highlightTokenShape} from "./util.js";
export function getHexSizeSupportTokenGridCenter(token) { export function getHexSizeSupportTokenGridCenter(token) {
const tokenCenterOffset = CONFIG.hexSizeSupport.getCenterOffset(token) const tokenCenterOffset = CONFIG.hexSizeSupport.getCenterOffset(token)
@@ -27,7 +27,7 @@ export function measureDistances(segments, entity, shape, options={}) {
const newSegments = segments.slice(firstNewSegmentIndex); const newSegments = segments.slice(firstNewSegmentIndex);
const distances = previousSegments.map(segment => segment.ray.dragRulerVisitedSpaces[segment.ray.dragRulerVisitedSpaces.length - 1].distance); const distances = previousSegments.map(segment => segment.ray.dragRulerVisitedSpaces[segment.ray.dragRulerVisitedSpaces.length - 1].distance);
previousSegments.forEach(segment => segment.ray.terrainRulerVisitedSpaces = duplicate(segment.ray.dragRulerVisitedSpaces)); 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) if (previousSegments.length > 0)
opts.terrainRulerInitialState = previousSegments[previousSegments.length - 1].ray.dragRulerFinalState; opts.terrainRulerInitialState = previousSegments[previousSegments.length - 1].ray.dragRulerFinalState;
return distances.concat(terrainRuler.measureDistances(newSegments, opts)); return distances.concat(terrainRuler.measureDistances(newSegments, opts));
+62 -12
View File
@@ -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 {moveWithoutAnimation, togglePathfinding} from "./keybindings.js";
import {debugGraphics} from "./main.js"; import {debugGraphics} from "./main.js";
import {settingsKey} from "./settings.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 * as GridlessPathfinding from "../wasm/gridless_pathfinding.js";
import {PriorityQueueSet, ProcessOnceQueue} from "./data_structures.js"; import {PriorityQueueSet, ProcessOnceQueue} from "./data_structures.js";
import {buildCostFunction} from "./api.js";
class CacheLayer { class CacheLayer {
constructor(tokenData, cacheId) { constructor(tokenData, cacheId) {
@@ -79,7 +80,18 @@ class Cache {
*/ */
getCacheLayer(token) { getCacheLayer(token) {
const tokenData = buildTokenData(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); let cacheLayer = this.layers.get(cacheId);
// If we don't already have a cache layer for this cache ID, create one now // If we don't already have a cache layer for this cache ID, create one now
if (!cacheLayer) { if (!cacheLayer) {
@@ -255,13 +267,37 @@ export function findPath(from, to, token, previousWaypoints) {
const path = []; const path = [];
let currentNode = firstNode; let currentNode = firstNode;
while (currentNode) { while (currentNode) {
// TODO Check if the distance doesn't change
if (path.length >= 2 && !stepCollidesWithWall(path[path.length - 2], currentNode.node, cacheLayer.tokenData)) { 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 // Replace last waypoint if the current waypoint leads to a valid path that isn't longer than the old path
path[path.length - 1] = {x: currentNode.node.x, y: currentNode.node.y}; if (window.terrainRuler) {
} else { let startNode = getCenterFromGridPositionObj(path[path.length - 2]);
path.push({x: currentNode.node.x, y: currentNode.node.y}); 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; currentNode = currentNode.next;
} }
return path; return path;
@@ -295,11 +331,25 @@ function getNode(pos, cacheLayer, initialize = true) {
// TODO Work with pixels instead of grid locations // TODO Work with pixels instead of grid locations
if (!stepCollidesWithWall(pos, neighborPos, cacheLayer.tokenData)) { if (!stepCollidesWithWall(pos, neighborPos, cacheLayer.tokenData)) {
const isDiagonal = node.x !== neighborPos.x && node.y !== neighborPos.y && canvas.grid.type === CONST.GRID_TYPES.SQUARE; 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); 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}); node.edges.push({target: neighbor, cost: edgeCost});
} }
} }
+13 -9
View File
@@ -167,15 +167,19 @@ export function getAreaFromPositionAndShape(position, shape) {
} }
export function getTokenShape(token) { 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}] return [{x: 0, y: 0}]
} }
else if (token.scene.data.gridType === CONST.GRID_TYPES.SQUARE) { else if (scene.data.gridType === CONST.GRID_TYPES.SQUARE) {
const topOffset = -Math.floor(token.data.height / 2) const topOffset = -Math.floor(tokenData.height / 2)
const leftOffset = -Math.floor(token.data.width / 2) const leftOffset = -Math.floor(tokenData.width / 2)
const shape = [] const shape = []
for (let y = 0;y < token.data.height;y++) { for (let y = 0;y < tokenData.height;y++) {
for (let x = 0;x < token.data.width;x++) { for (let x = 0;x < tokenData.width;x++) {
shape.push({x: x + leftOffset, y: y + topOffset}) shape.push({x: x + leftOffset, y: y + topOffset})
} }
} }
@@ -183,8 +187,8 @@ export function getTokenShape(token) {
} }
else { else {
// Hex grids // Hex grids
if (game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) { if (game.modules.get("hex-size-support")?.active && tokenData.hexSizeSupport.altSnappingFlag) {
const borderSize = token.data.flags["hex-size-support"].borderSize; const borderSize = tokenData.hexSizeSupport.borderSize;
let shape = [{x: 0, y: 0}]; let shape = [{x: 0, y: 0}];
if (borderSize >= 2) if (borderSize >= 2)
shape = shape.concat([{x: 0, y: -1}, {x: -1, y: -1}]); shape = shape.concat([{x: 0, y: -1}, {x: -1, y: -1}]);
@@ -193,7 +197,7 @@ export function getTokenShape(token) {
if (borderSize >= 4) 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}]) 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); shape.forEach(space => space.y *= -1);
if (canvas.grid.grid.options.columns) if (canvas.grid.grid.options.columns)
shape = shape.map(space => {return {x: space.y, y: space.x}}); shape = shape.map(space => {return {x: space.y, y: space.x}});
+78
View File
@@ -2,6 +2,15 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "block-buffer"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.9.1" version = "3.9.1"
@@ -24,6 +33,45 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "cpufeatures"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "generic-array"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
dependencies = [
"typenum",
"version_check",
]
[[package]] [[package]]
name = "gridless-pathfinding" name = "gridless-pathfinding"
version = "1.12.8" version = "1.12.8"
@@ -31,6 +79,7 @@ dependencies = [
"console_error_panic_hook", "console_error_panic_hook",
"js-sys", "js-sys",
"rustc-hash", "rustc-hash",
"sha1",
"wasm-bindgen", "wasm-bindgen",
] ]
@@ -49,6 +98,12 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.14" version = "0.4.14"
@@ -82,6 +137,17 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "sha1"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77f4e7f65455545c2153c1253d25056825e77ee2533f0e41deb65a93a34852f"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.86" version = "1.0.86"
@@ -93,12 +159,24 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "typenum"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.2" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.79" version = "0.2.79"
+1
View File
@@ -16,4 +16,5 @@ lto = true
console_error_panic_hook = "0.1.7" console_error_panic_hook = "0.1.7"
js-sys = "0.3.56" js-sys = "0.3.56"
rustc-hash = "1.1.0" rustc-hash = "1.1.0"
sha1 = "0.10.1"
wasm-bindgen = "0.2.79" wasm-bindgen = "0.2.79"
+9
View File
@@ -1,4 +1,5 @@
use js_sys::Array; use js_sys::Array;
use sha1::{Sha1, Digest};
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use crate::{ use crate::{
@@ -262,6 +263,14 @@ pub fn debug_get_pathfinding_points(pathfinder: &Pathfinder) -> Array {
.collect() .collect()
} }
#[allow(dead_code)]
#[wasm_bindgen]
pub fn sha1(input: &str) -> String {
let mut hasher = Sha1::new();
hasher.update(input);
format!("{:x}", hasher.finalize())
}
trait IteratePath { trait IteratePath {
fn iter_path(&self) -> PathIterator; fn iter_path(&self) -> PathIterator;
} }