Compare commits

...

11 Commits

Author SHA1 Message Date
Manuel Vögele df4c515fcd Release v1.12.3 2022-03-07 16:53:48 +01:00
Manuel Vögele 02dfd8db0d Only reclaculate the ruler if it's a Drag Ruler (fixes #161) 2022-03-07 14:39:31 +01:00
Manuel Vögele ddf89f9499 Add support for the wall height module in the gridless pathfinder 2022-03-07 12:40:16 +01:00
Jonathan Calvert 0dfdb23bfb Levels Compatibility: Clear pathfinding cache after changing elevation (#173) 2022-03-07 12:39:16 +01:00
Jonathan Calvert 3f2e9e1a3e Fix one-way wall collision detection (fixes #167) (#172) 2022-03-07 12:38:30 +01:00
Jonathan Calvert 576db2dc5a Pathfinding Improvements: Bound Algorithm to Canvas (#168) 2022-02-28 09:52:12 +01:00
EndlesNights b031acaa6e dnd 4e system support (#162) 2022-02-28 09:42:10 +01:00
Manuel Vögele ee956abd78 Update .gitignore 2022-02-17 11:15:40 +01:00
Manuel Vögele 1334703877 Release v1.12.2 2022-02-17 00:58:33 +01:00
Manuel Vögele d6cccf4466 Ensure pathfinding on gridless is possible by always generating a pathfinding point when a virtual wall is being inserted 2022-02-17 00:53:51 +01:00
Manuel Vögele 074d2f5052 Disable pathfinding when the hotkey for moving tokens without pathfinding is enabled (fixes #158) 2022-02-17 00:47:13 +01:00
12 changed files with 169 additions and 54 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
foundry-*.js /foundry*.js
artifact/ artifact/
wasm/ wasm/
*.lock
+17
View File
@@ -1,3 +1,20 @@
## 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
### Bugfixes
- Fixed a bug where the pathfinder on gridless scenes sometimes wasn't able to find a way around corners with specific angles
- Pathfinding will now be disabled when the hotkey to move tokens without animation is being pressed, to allow GMs to move their tokens through walls
## 1.12.1 ## 1.12.1
### Hotfix ### Hotfix
- Version 1.12.0 was incorrectly packaged, which caused it to fail to load - Version 1.12.0 was incorrectly packaged, which caused it to fail to load
+10
View File
@@ -113,6 +113,16 @@ function handleDisableSnap(event) {
function handleMoveWithoutAnimation(event) { function handleMoveWithoutAnimation(event) {
moveWithoutAnimation = !event.up; moveWithoutAnimation = !event.up;
const ruler = canvas.controls.ruler;
if (!ruler?.isDragRuler)
return false;
if (ruler._state !== Ruler.STATES.MEASURING)
return false;
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
ruler.dragRulerSendState();
return false;
} }
function handleTogglePathfinding(event) { function handleTogglePathfinding(event) {
+5 -2
View File
@@ -7,7 +7,7 @@ import {disableSnap, registerKeybindings} from "./keybindings.js";
import {libWrapper} from "./libwrapper_shim.js"; import {libWrapper} from "./libwrapper_shim.js";
import {performMigrations} from "./migration.js" import {performMigrations} from "./migration.js"
import {removeLastHistoryEntryIfAt, resetMovementHistory} from "./movement_tracking.js"; import {removeLastHistoryEntryIfAt, resetMovementHistory} from "./movement_tracking.js";
import {wipePathfindingCache} from "./pathfinding.js"; import {wipePathfindingCache, initializePathfinding} from "./pathfinding.js";
import {extendRuler} from "./ruler.js"; import {extendRuler} from "./ruler.js";
import {registerSettings, RightClickAction, settingsKey} from "./settings.js" import {registerSettings, RightClickAction, settingsKey} from "./settings.js"
import {recalculate} from "./socket.js"; import {recalculate} from "./socket.js";
@@ -21,7 +21,10 @@ export let debugGraphics = undefined;
initGridlessPathfinding().then(() => { initGridlessPathfinding().then(() => {
Hooks.on("canvasInit", wipePathfindingCache); Hooks.on("canvasInit", wipePathfindingCache);
Hooks.on("canvasReady", wipePathfindingCache); Hooks.on("canvasReady", () => {
wipePathfindingCache();
initializePathfinding();
});
Hooks.on("createWall", wipePathfindingCache); Hooks.on("createWall", wipePathfindingCache);
Hooks.on("updateWall", wipePathfindingCache); Hooks.on("updateWall", wipePathfindingCache);
Hooks.on("deleteWall", wipePathfindingCache); Hooks.on("deleteWall", wipePathfindingCache);
+42 -15
View File
@@ -1,5 +1,5 @@
import {getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js"; import {getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
import {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 {getSnapPointForTokenObj, iterPairs} from "./util.js"; import {getSnapPointForTokenObj, iterPairs} from "./util.js";
@@ -7,23 +7,29 @@ import {getSnapPointForTokenObj, iterPairs} from "./util.js";
import * as GridlessPathfinding from "../wasm/gridless_pathfinding.js" import * as GridlessPathfinding from "../wasm/gridless_pathfinding.js"
let cachedNodes = undefined; let cachedNodes = undefined;
let cacheElevation;
let use5105 = false; let use5105 = false;
let gridlessPathfinders = new Map(); let gridlessPathfinders = new Map();
let gridWidth, gridHeight;
export function isPathfindingEnabled() { export function isPathfindingEnabled() {
if (this.user !== game.user) if (this.user !== game.user)
return false; return false;
if (!game.user.isGM && !game.settings.get(settingsKey, "allowPathfinding")) if (!game.user.isGM && !game.settings.get(settingsKey, "allowPathfinding"))
return false; return false;
if (moveWithoutAnimation)
return false;
return game.settings.get(settingsKey, "autoPathfinding") != togglePathfinding; return game.settings.get(settingsKey, "autoPathfinding") != togglePathfinding;
} }
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);
if (!pathfinder) { if (!pathfinder) {
pathfinder = GridlessPathfinding.initialize(canvas.walls.placeables, tokenSize); pathfinder = GridlessPathfinding.initialize(canvas.walls.placeables, tokenSize, token.data.elevation, Boolean(game.modules.get("levels")?.active));
gridlessPathfinders.set(tokenSize, pathfinder); gridlessPathfinders.set(tokenSize, pathfinder);
} }
paintGridlessPathfindingDebug(pathfinder); paintGridlessPathfindingDebug(pathfinder);
@@ -38,7 +44,7 @@ export function findPath(from, to, token, previousWaypoints) {
let currentNode = lastNode; let currentNode = lastNode;
while (currentNode) { while (currentNode) {
// TODO Check if the distance doesn't change // TODO Check if the distance doesn't change
if (path.length >= 2 && !stepCollidesWithWall(currentNode.node, path[path.length - 2], 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 // Replace last waypoint if the current waypoint leads to a valid path
path[path.length - 1] = {x: currentNode.node.x, y: currentNode.node.y}; path[path.length - 1] = {x: currentNode.node.x, y: currentNode.node.y};
else else
@@ -49,16 +55,6 @@ export function findPath(from, to, token, previousWaypoints) {
} }
} }
export function wipePathfindingCache() {
cachedNodes = undefined;
for (const pathfinder of gridlessPathfinders.values()) {
GridlessPathfinding.free(pathfinder);
}
gridlessPathfinders.clear();
if (debugGraphics)
debugGraphics.removeChildren().forEach(c => c.destroy());
}
function getNode(pos, token, initialize=true) { function getNode(pos, token, initialize=true) {
pos = {layer: 0, ...pos}; // Copy pos and set pos.layer to the default value if it's unset pos = {layer: 0, ...pos}; // Copy pos and set pos.layer to the default value if it's unset
if (!cachedNodes) if (!cachedNodes)
@@ -75,10 +71,12 @@ function getNode(pos, token, initialize=true) {
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) if (neighborPos.x < 0 || neighborPos.y < 0 || neighborPos.x > gridWidth || neighborPos.y > gridHeight) {
continue; continue;
}
// TODO Work with pixels instead of grid locations // TODO Work with pixels instead of grid locations
if (!stepCollidesWithWall(pos, neighborPos, token)) { if (!stepCollidesWithWall(neighborPos, pos, token)) {
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 targetLayer = pos.layer; let targetLayer = pos.layer;
if (use5105 && isDiagonal) if (use5105 && isDiagonal)
@@ -156,6 +154,35 @@ function stepCollidesWithWall(from, to, token) {
return canvas.walls.checkCollision(new Ray(stepStart, stepEnd)); return canvas.walls.checkCollision(new Ray(stepStart, stepEnd));
} }
export function wipePathfindingCache() {
cachedNodes = undefined;
for (const pathfinder of gridlessPathfinders.values()) {
GridlessPathfinding.free(pathfinder);
}
gridlessPathfinders.clear();
if (debugGraphics)
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);
}
function paintGriddedPathfindingDebug(lastNode, token) { function paintGriddedPathfindingDebug(lastNode, token) {
if (!CONFIG.debug.dragRuler) if (!CONFIG.debug.dragRuler)
return; return;
+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);
} }
+8 -5
View File
@@ -4,13 +4,15 @@ export function getDefaultSpeedAttribute() {
return "actor.data.data.attribs.mov.value"; return "actor.data.data.attribs.mov.value";
case "dcc": case "dcc":
return "actor.data.data.attributes.speed.value"; return "actor.data.data.attributes.speed.value";
case "dnd4e":
return "actor.data.data.movement.walk.value";
case "dnd5e": case "dnd5e":
return "actor.data.data.attributes.movement.walk" return "actor.data.data.attributes.movement.walk";
case "lancer": case "lancer":
return "actor.data.data.derived.speed" return "actor.data.data.derived.speed";
case "pf1": case "pf1":
case "D35E": case "D35E":
return "actor.data.data.attributes.speed.land.total" return "actor.data.data.attributes.speed.land.total";
case "sfrpg": case "sfrpg":
return "actor.data.data.attributes.speed.value"; return "actor.data.data.attributes.speed.value";
case "shadowrun5e": case "shadowrun5e":
@@ -28,6 +30,7 @@ export function getDefaultDashMultiplier() {
case "swade": case "swade":
return 0 return 0
case "dcc": case "dcc":
case "dnd4e":
case "dnd5e": case "dnd5e":
case "lancer": case "lancer":
case "pf1": case "pf1":
@@ -35,9 +38,9 @@ export function getDefaultDashMultiplier() {
case "sfrpg": case "sfrpg":
case "shadowrun5e": case "shadowrun5e":
case "ds4": case "ds4":
return 2 return 2;
case "CoC7": case "CoC7":
return 5; return 5;
} }
return 0 return 0;
} }
+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.1", "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.1/drag-ruler-1.12.1.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.1" 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.1" 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
+60 -4
View File
@@ -23,6 +23,8 @@ extern "C" {
extern "C" { extern "C" {
pub type JsWall; pub type JsWall;
pub type JsWallData; pub type JsWallData;
pub type JsWallFlags;
pub type JsWallHeight;
#[wasm_bindgen(method, getter)] #[wasm_bindgen(method, getter)]
fn data(this: &JsWall) -> JsWallData; fn data(this: &JsWall) -> JsWallData;
@@ -38,6 +40,18 @@ extern "C" {
#[wasm_bindgen(method, getter, js_name = "move")] #[wasm_bindgen(method, getter, js_name = "move")]
fn move_type(this: &JsWallData) -> WallSenseType; fn move_type(this: &JsWallData) -> WallSenseType;
#[wasm_bindgen(method, getter)]
fn flags(this: &JsWallData) -> JsWallFlags;
#[wasm_bindgen(method, getter, js_name = "wallHeight")]
fn wall_height(this: &JsWallFlags) -> Option<JsWallHeight>;
#[wasm_bindgen(method, getter, js_name = "wallHeightTop")]
fn top(this: &JsWallHeight) -> Option<f64>;
#[wasm_bindgen(method, getter, js_name = "wallHeightBottom")]
fn bottom(this: &JsWallHeight) -> Option<f64>;
} }
#[wasm_bindgen] #[wasm_bindgen]
@@ -120,6 +134,38 @@ impl TryFrom<usize> for WallSenseType {
} }
} }
#[derive(Debug, Copy, Clone)]
pub struct WallHeight {
pub top: f64,
pub bottom: f64,
}
impl Default for WallHeight {
fn default() -> Self {
Self {
top: f64::INFINITY,
bottom: f64::NEG_INFINITY,
}
}
}
impl From<Option<JsWallHeight>> for WallHeight {
fn from(height: Option<JsWallHeight>) -> Self {
let height = height
.map(|height| (height.top(), height.bottom()))
.unwrap_or((None, None));
let top = height.0.unwrap_or(WallHeight::default().top);
let bottom = height.1.unwrap_or(WallHeight::default().bottom);
Self { top, bottom }
}
}
impl WallHeight {
pub fn contains(&self, height: f64) -> bool {
self.top >= height && self.bottom <= height
}
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct Wall { pub struct Wall {
pub p1: Point, pub p1: Point,
@@ -127,6 +173,7 @@ pub struct Wall {
pub door_type: DoorType, pub door_type: DoorType,
pub door_state: DoorState, pub door_state: DoorState,
pub move_type: WallSenseType, pub move_type: WallSenseType,
pub height: WallHeight,
} }
impl Wall { impl Wall {
@@ -136,6 +183,7 @@ impl Wall {
door_type: DoorType, door_type: DoorType,
door_state: DoorState, door_state: DoorState,
move_type: WallSenseType, move_type: WallSenseType,
height: WallHeight,
) -> Self { ) -> Self {
Self { Self {
p1, p1,
@@ -143,6 +191,7 @@ impl Wall {
door_type, door_type,
door_state, door_state,
move_type, move_type,
height,
} }
} }
@@ -156,29 +205,36 @@ impl Wall {
} }
impl Wall { impl Wall {
fn from_js(wall: &JsWall) -> Self { fn from_js(wall: &JsWall, enable_height: bool) -> Self {
let data = wall.data(); let data = wall.data();
let mut c = data.c(); let mut c = data.c();
c.iter_mut().for_each(|val| *val = val.round()); c.iter_mut().for_each(|val| *val = val.round());
let height = if enable_height {
data.flags().wall_height().into()
}
else {
WallHeight::default()
};
Self::new( Self::new(
Point::new(c[0], c[1]), Point::new(c[0], c[1]),
Point::new(c[2], c[3]), Point::new(c[2], c[3]),
data.door_type(), data.door_type(),
data.door_state(), data.door_state(),
data.move_type(), data.move_type(),
height,
) )
} }
} }
#[allow(dead_code)] #[allow(dead_code)]
#[wasm_bindgen] #[wasm_bindgen]
pub fn initialize(js_walls: Vec<JsValue>, token_size: f64) -> Pathfinder { pub fn initialize(js_walls: Vec<JsValue>, token_size: f64, token_elevation: f64, enable_height: bool) -> Pathfinder {
let mut walls = Vec::with_capacity(js_walls.len()); let mut walls = Vec::with_capacity(js_walls.len());
for wall in js_walls { for wall in js_walls {
let wall = JsWall::from(wall); let wall = JsWall::from(wall);
walls.push(Wall::from_js(&wall)); walls.push(Wall::from_js(&wall, enable_height));
} }
Pathfinder::initialize(walls, token_size) Pathfinder::initialize(walls, token_size, token_elevation)
} }
#[allow(dead_code)] #[allow(dead_code)]
+18 -22
View File
@@ -148,7 +148,7 @@ pub struct Pathfinder {
} }
impl Pathfinder { impl Pathfinder {
pub fn initialize<I>(walls: I, token_size: f64) -> Self pub fn initialize<I>(walls: I, token_size: f64, token_elevation: f64) -> Self
where where
I: IntoIterator<Item = Wall>, I: IntoIterator<Item = Wall>,
{ {
@@ -162,6 +162,9 @@ impl Pathfinder {
if wall.is_door() && wall.is_open() { if wall.is_door() && wall.is_open() {
continue; continue;
} }
if !wall.height.contains(token_elevation) {
continue;
}
let x_diff = wall.p2.x - wall.p1.x; let x_diff = wall.p2.x - wall.p1.x;
let y_diff = wall.p2.y - wall.p1.y; let y_diff = wall.p2.y - wall.p1.y;
let p1_angle = y_diff.atan2(x_diff).rem_euclid(2.0 * PI); let p1_angle = y_diff.atan2(x_diff).rem_euclid(2.0 * PI);
@@ -188,18 +191,13 @@ impl Pathfinder {
if angle_diff <= PI { if angle_diff <= PI {
continue; continue;
} }
{ let angle_between = angle_diff / 2.0 + angle1;
let angle_between = angle_diff / 2.0 + angle1; nodes.push(calc_pathfinding_node(
let pathfinding_node = calc_pathfinding_node( point,
point, angle_between,
angle_between, distance_from_walls,
distance_from_walls, &mut line_segments,
&mut line_segments, ));
);
if angle_diff > 1.5 * PI {
nodes.push(pathfinding_node);
}
}
nodes.push(calc_pathfinding_node( nodes.push(calc_pathfinding_node(
point, point,
angle1 + 0.5 * PI, angle1 + 0.5 * PI,
@@ -219,15 +217,13 @@ impl Pathfinder {
if angle_diff <= PI { if angle_diff <= PI {
continue; continue;
} }
if angle_diff > 1.5 * PI { let angle_between = angle_diff / 2.0 + angle1;
let angle_between = angle_diff / 2.0 + angle1; nodes.push(calc_pathfinding_node(
nodes.push(calc_pathfinding_node( point,
point, angle_between,
angle_between, distance_from_walls,
distance_from_walls, &mut line_segments,
&mut line_segments, ));
));
}
nodes.push(calc_pathfinding_node( nodes.push(calc_pathfinding_node(
point, point,
angle1 + 0.5 * PI, angle1 + 0.5 * PI,