Merge branch 'develop' into pathfinding-difficult-terrain
This commit is contained in:
@@ -1,3 +1,8 @@
|
|||||||
|
## 1.12.6
|
||||||
|
### Bugfixes
|
||||||
|
- Measured templates no longer snap to a virtual grid on gridless scenes (this was a regression introduced in 1.12.5)
|
||||||
|
|
||||||
|
|
||||||
## 1.12.5
|
## 1.12.5
|
||||||
### Compatibility
|
### Compatibility
|
||||||
- Drag Ruler is now compatible with DF Template Enhancements
|
- Drag Ruler is now compatible with DF Template Enhancements
|
||||||
|
|||||||
+57
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* A combination queue/set where the elements are ordered (in ascending order, according to the given priority function)
|
* A combination queue/set where the elements are ordered (in ascending order, according to the given priority function)
|
||||||
* and unique (according to the given elementMatcher).
|
* and unique (according to the given elementMatcher).
|
||||||
*
|
*
|
||||||
* If an element is added to the set and an equivalent element already exists, the lower-priority one is discarded.
|
* If an element is added to the set and an equivalent element already exists, the lower-priority one is discarded.
|
||||||
*/
|
*/
|
||||||
export class PriorityQueueSet {
|
export class PriorityQueueSet {
|
||||||
@@ -80,3 +80,59 @@ export class PriorityQueueSet {
|
|||||||
return first?.value;
|
return first?.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue that will only ever accept elements with a given value once. Elements must have a "value" field, the
|
||||||
|
* JSON representation of which will be used as the key to match
|
||||||
|
*/
|
||||||
|
export class ProcessOnceQueue {
|
||||||
|
constructor() {
|
||||||
|
this.first = null;
|
||||||
|
this.last = null;
|
||||||
|
this.previouslyQueued = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove everything from the queue and forget all the previously-queued items
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.first = null;
|
||||||
|
this.last = null;
|
||||||
|
this.previouslyQueued.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
push(element) {
|
||||||
|
if (this.previouslyQueued.has(element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.previouslyQueued.add(element);
|
||||||
|
|
||||||
|
const newNode = {
|
||||||
|
value: element,
|
||||||
|
next: null,
|
||||||
|
previous: null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.first) {
|
||||||
|
this.first = newNode;
|
||||||
|
this.last = newNode;
|
||||||
|
} else {
|
||||||
|
this.last.next = newNode;
|
||||||
|
newNode.previous = this.last;
|
||||||
|
this.last = newNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pop() {
|
||||||
|
const node = this.first;
|
||||||
|
this.first = node?.next;
|
||||||
|
if (!node?.next) {
|
||||||
|
this.last = null;
|
||||||
|
}
|
||||||
|
return node?.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNext() {
|
||||||
|
return !!this.first;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* Functions taken directly from the hex-size-support module (https://github.com/Ourobor/Hex-Size-Support/releases/tag/1.1.0).
|
||||||
|
* Unless otherwise stated, these functions are taken as-is.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Altered version of this function.
|
||||||
|
* - Instead of taking a token as a parameter to retrieve the altOrientationFlag, receive the flag value directly
|
||||||
|
* - Instead of taking a grid parameter, get the grid value from the globas canvas
|
||||||
|
*/
|
||||||
|
export function findVertexSnapPoint(x, y, altOrientationFlag) {
|
||||||
|
const grid = canvas.grid.grid;
|
||||||
|
if (grid.columns) {
|
||||||
|
return findSnapPointCols(x, y, grid.h, grid.w, altOrientationFlag);
|
||||||
|
} else {
|
||||||
|
return findSnapPointRows(x, y, grid.h, grid.w, altOrientationFlag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findSnapPointRows(x, y, h, w, alt) {
|
||||||
|
let xOffset = 0.0
|
||||||
|
if (canvas.grid.grid.even) {
|
||||||
|
xOffset = -0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
let yOffset1 = 0.75
|
||||||
|
let yOffset2 = 0.00
|
||||||
|
if (alt) {
|
||||||
|
yOffset1 = 0.25
|
||||||
|
yOffset2 = 1.00
|
||||||
|
}
|
||||||
|
|
||||||
|
let row1 = calculateSnapPointsRows(x, y, h, w, 0.5 + xOffset, yOffset1);
|
||||||
|
let row2 = calculateSnapPointsRows(x, y, h, w, 1.0 + xOffset, yOffset2);
|
||||||
|
|
||||||
|
let dist1 = Math.pow((row1.x - x), 2) + Math.pow((row1.y - y), 2)
|
||||||
|
let dist2 = Math.pow((row2.x - x), 2) + Math.pow((row2.y - y), 2)
|
||||||
|
|
||||||
|
if (dist1 < dist2) {
|
||||||
|
return row1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return row2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateSnapPointsRows(x, y, h, w, xOff, yOff) {
|
||||||
|
let c = Math.floor(((x + ((0.5 - xOff) * w)) / w) + 1)
|
||||||
|
let r = Math.floor(((y + ((0.75 - yOff) * h)) / (1.5 * h)) + 1)
|
||||||
|
|
||||||
|
let snapX = (c * w) - ((1 - xOff) * w)
|
||||||
|
let snapY = (r * h * 1.5) - ((1.5 - yOff) * h)
|
||||||
|
|
||||||
|
return {x: snapX, y: snapY}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findSnapPointCols(x, y, h, w, alt) {
|
||||||
|
let yOffset = 0.0
|
||||||
|
if (canvas.grid.grid.even) {
|
||||||
|
yOffset = -0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
let xOffset1 = 0.25
|
||||||
|
let xOffset2 = 1.00
|
||||||
|
if (alt) {
|
||||||
|
xOffset1 = 0.75
|
||||||
|
xOffset2 = 0.00
|
||||||
|
}
|
||||||
|
|
||||||
|
let row1 = calculateSnapPointsCols(x, y, h, w, xOffset1, 0.5 + yOffset);
|
||||||
|
let row2 = calculateSnapPointsCols(x, y, h, w, xOffset2, 1.0 + yOffset);
|
||||||
|
|
||||||
|
let dist1 = Math.pow((row1.x - x), 2) + Math.pow((row1.y - y), 2)
|
||||||
|
let dist2 = Math.pow((row2.x - x), 2) + Math.pow((row2.y - y), 2)
|
||||||
|
|
||||||
|
if (dist1 < dist2) {
|
||||||
|
return row1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return row2
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateSnapPointsCols(x, y, h, w, xOff, yOff) {
|
||||||
|
let c = Math.floor(((x + ((0.75 - xOff) * w)) / (1.5 * w)) + 1)
|
||||||
|
let r = Math.floor(((y + ((0.5 - yOff) * h)) / h) + 1)
|
||||||
|
|
||||||
|
let snapX = (c * w * 1.5) - ((1.5 - xOff) * w)
|
||||||
|
let snapY = (r * h) - ((1 - yOff) * h)
|
||||||
|
|
||||||
|
return {x: snapX, y: snapY}
|
||||||
|
}
|
||||||
+16
-1
@@ -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, initializePathfinding} from "./pathfinding.js";
|
import {wipePathfindingCache, initializePathfinding, startBackgroundCaching} 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";
|
||||||
@@ -28,6 +28,21 @@ initGridlessPathfinding().then(() => {
|
|||||||
Hooks.on("createWall", wipePathfindingCache);
|
Hooks.on("createWall", wipePathfindingCache);
|
||||||
Hooks.on("updateWall", wipePathfindingCache);
|
Hooks.on("updateWall", wipePathfindingCache);
|
||||||
Hooks.on("deleteWall", wipePathfindingCache);
|
Hooks.on("deleteWall", wipePathfindingCache);
|
||||||
|
|
||||||
|
// Whenever the current user selects a token, start caching
|
||||||
|
Hooks.on("controlToken", (token, controlled) => {
|
||||||
|
if (controlled) {
|
||||||
|
startBackgroundCaching(token);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Whenever a token the current user controls updates, start caching
|
||||||
|
Hooks.on("updateToken", (document) => {
|
||||||
|
const token = document.object;
|
||||||
|
if (token._controlled) {
|
||||||
|
startBackgroundCaching(token);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Hooks.once("init", () => {
|
Hooks.once("init", () => {
|
||||||
|
|||||||
+265
-78
@@ -2,60 +2,221 @@ import {getCenterFromGridPositionObj, getGridPositionFromPixelsObj, getPixelsFro
|
|||||||
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 {getSnapPointForTokenObj, getTokenShape, getTokenShapeId, iterPairs} from "./util.js";
|
import {buildSnapPointTokenData, getSnapPointForTokenDataObj, isModuleActive, iterPairs} from "./util.js";
|
||||||
|
|
||||||
import * as GridlessPathfinding from "../wasm/gridless_pathfinding.js";
|
import * as GridlessPathfinding from "../wasm/gridless_pathfinding.js";
|
||||||
import {PriorityQueueSet} from "./data_structures.js";
|
import {PriorityQueueSet, ProcessOnceQueue} from "./data_structures.js";
|
||||||
import { buildCostFunction } from "./api.js";
|
import {buildCostFunction} from "./api.js";
|
||||||
|
|
||||||
|
class CacheLayer {
|
||||||
|
constructor(tokenData, cacheId) {
|
||||||
|
this.tokenData = tokenData;
|
||||||
|
this.cacheId = cacheId;
|
||||||
|
this.queue = new ProcessOnceQueue();
|
||||||
|
|
||||||
|
this.buildNodes();
|
||||||
|
this.registerUse();
|
||||||
|
}
|
||||||
|
|
||||||
|
buildNodes() {
|
||||||
|
this.nodes = new Array(gridHeight);
|
||||||
|
for (let y = 0; y < gridHeight; y++) {
|
||||||
|
this.nodes[y] = new Array(gridWidth);
|
||||||
|
for (let x = 0; x < gridWidth; x++) {
|
||||||
|
this.nodes[y][x] = {x, y};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerUse() {
|
||||||
|
this.lastUsed = Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to hold all the cached node data, and functions to deal with caching.
|
||||||
|
*
|
||||||
|
* Since pathfinding can depend on several factors, e.g. the token's size, we keep
|
||||||
|
* several caches, keyed by all the data relevant to pathfinding. If we already have
|
||||||
|
* the maximum number of caches and we need to create another one, we discard the
|
||||||
|
* one not used for the longest.
|
||||||
|
*
|
||||||
|
* When we select a token, or a token we have selected updates, we start caching
|
||||||
|
* in the background so, when we do start pathfinding, it's very performant.
|
||||||
|
*
|
||||||
|
* Background caching starts by trying to run an idle process (when the browser is
|
||||||
|
* otherwise not busy), but if it can't do that after an amount of time (e.g. the
|
||||||
|
* CPU is very slow and is busy) then we instead start caching a few nodes each
|
||||||
|
* frame.
|
||||||
|
*/
|
||||||
class Cache {
|
class Cache {
|
||||||
static maxCacheIds = 5;
|
static maxCacheLayers = 5;
|
||||||
|
static maxBackgroundCachingMillis = 10;
|
||||||
|
static maxAnimationCachingMillis = 5;
|
||||||
|
static backgroundCachingTimeoutMillis = 200;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.nodes = new Map();
|
this.layers = new Map();
|
||||||
this.lastUsed = new Map();
|
this.background = {
|
||||||
|
nextJobId: null,
|
||||||
|
nextTimeoutId: null,
|
||||||
|
nextAnimationFrameId: null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.nodes.clear();
|
this.layers.clear();
|
||||||
this.lastUsed.clear();
|
if (this.background.nextJobId) {
|
||||||
|
window.cancelIdleCallback(this.background.nextJobId);
|
||||||
|
this.background.nextJobId = null;
|
||||||
|
}
|
||||||
|
this.cancelTimeout();
|
||||||
|
this.cancelAnimationFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the cache associated with the given cache ID, creating a new one
|
* Retrieve the cache layer for this token, using information that can make a difference to the pathfinding algorithm
|
||||||
* if we don't already have one
|
* If a layer that suits this token doesn't exist, create one
|
||||||
*/
|
*/
|
||||||
getCachedNodes(cacheId) {
|
getCacheLayer(token) {
|
||||||
// Track that we've last used this cache right now
|
const tokenData = buildTokenData(token);
|
||||||
this.lastUsed.set(cacheId, Date.now());
|
const cacheId = JSON.stringify(tokenData);
|
||||||
|
let cacheLayer = this.layers.get(cacheId);
|
||||||
// Get the nodes for the cacheId. If we don't already have one, create one
|
// If we don't already have a cache layer for this cache ID, create one now
|
||||||
let cachedNodes = this.nodes.get(cacheId);
|
if (!cacheLayer) {
|
||||||
if (!cachedNodes) {
|
// Check if we already have the max number of layers. If we do,
|
||||||
cachedNodes = new Array(gridHeight);
|
// get rid of the one that hasn't been used for the longest
|
||||||
for (let y = 0; y < gridHeight; y++) {
|
if (this.layers.size >= Cache.maxCacheLayers) {
|
||||||
cachedNodes[y] = new Array(gridWidth);
|
const oldestCache = Array.from(this.layers.values())
|
||||||
for (let x = 0; x < gridWidth; x++) {
|
.reduce((layer1, layer2) => (layer1?.lastUsed < layer2.lastUsed) ? layer1 : layer2, null);
|
||||||
cachedNodes[y][x] = {x, y};
|
this.layers.delete(oldestCache.cacheId);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.nodes.set(cacheId, cachedNodes);
|
|
||||||
|
|
||||||
// Since we're adding a new cache, check if we have too many and,
|
// Create the new cache
|
||||||
// if we do, get rid of the one that was last used longest ago
|
cacheLayer = new CacheLayer(tokenData, cacheId);
|
||||||
if (this.lastUsed.size > Cache.maxCacheIds) {
|
this.layers.set(cacheId, cacheLayer);
|
||||||
let oldest;
|
} else {
|
||||||
for (let entry of this.lastUsed) {
|
// Register that we're using this cache right now
|
||||||
if (!oldest || oldest[1] > entry[1]) {
|
cacheLayer.registerUse();
|
||||||
oldest = entry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.nodes.delete(oldest[0]);
|
|
||||||
this.lastUsed.delete(oldest[0]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return cachedNodes;
|
return cacheLayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start background caching from the token's current position
|
||||||
|
*/
|
||||||
|
startBackgroundCaching(token) {
|
||||||
|
const cacheLayer = this.getCacheLayer(token);
|
||||||
|
const tokenPosition = getGridPositionFromPixelsObj(token.position)
|
||||||
|
|
||||||
|
cacheLayer.queue.push(cacheLayer.nodes[tokenPosition.y][tokenPosition.x]);
|
||||||
|
|
||||||
|
this.scheduleBackgroundCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find if any of the caches have more nodes to background cache. If there is, then schedule a background
|
||||||
|
* caching job for that queue
|
||||||
|
*/
|
||||||
|
scheduleBackgroundCache() {
|
||||||
|
// If we already have a nextJobId, then don't start another one
|
||||||
|
if (this.background.nextJobId) return;
|
||||||
|
|
||||||
|
// Find the latest-used cache that has nodes left to cache
|
||||||
|
const latestCache = this.getLatestCacheWithNonEmptyQueue();
|
||||||
|
if (latestCache) {
|
||||||
|
this.background.nextJobId = window.requestIdleCallback(
|
||||||
|
() => this.runBackgroundCache(latestCache)
|
||||||
|
);
|
||||||
|
this.resetAnimationFrameTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a timeout which, if we reach the timeout time, will schedule a small amount of caching
|
||||||
|
* to be performed every frame. This timeout will be reset every time we perform background caching.
|
||||||
|
*/
|
||||||
|
resetAnimationFrameTimeout() {
|
||||||
|
this.cancelTimeout();
|
||||||
|
this.cancelAnimationFrame();
|
||||||
|
|
||||||
|
this.background.nextTimeoutId = window.setTimeout(
|
||||||
|
() => {
|
||||||
|
this.scheduleAnimationFrameCache();
|
||||||
|
this.background.nextTimeoutId = null;
|
||||||
|
},
|
||||||
|
Cache.backgroundCachingTimeoutMillis
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule a small amount of caching to be done just before the next frame renders
|
||||||
|
*/
|
||||||
|
scheduleAnimationFrameCache() {
|
||||||
|
const latestCache = this.getLatestCacheWithNonEmptyQueue();
|
||||||
|
if (latestCache) {
|
||||||
|
this.background.nextAnimationFrameId = window.requestAnimationFrame(
|
||||||
|
() => this.runAnimationCache(latestCache)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find which cache was last used and get its cache ID
|
||||||
|
*/
|
||||||
|
getLatestCacheWithNonEmptyQueue() {
|
||||||
|
return Array.from(this.layers.values())
|
||||||
|
.filter(layer => layer.queue.hasNext())
|
||||||
|
.reduce((layer1, layer2) => (layer1?.lastUsed > layer2.lastUsed) ? layer1 : layer2, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache nodes for a short time, and then schedule another idle job to cache more nodes
|
||||||
|
*/
|
||||||
|
runBackgroundCache(cacheLayer) {
|
||||||
|
const endTime = performance.now() + Cache.maxBackgroundCachingMillis;
|
||||||
|
while (cacheLayer.queue.hasNext() && performance.now() < endTime) {
|
||||||
|
this.cacheNextNode(cacheLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.background.nextJobId = null;
|
||||||
|
this.scheduleBackgroundCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache nodes for a very short time, then schedule to cache more nodes next frame
|
||||||
|
*/
|
||||||
|
runAnimationCache(cacheLayer) {
|
||||||
|
const endTime = performance.now() + Cache.maxAnimationCachingMillis;
|
||||||
|
while (cacheLayer.queue.hasNext() && performance.now() < endTime) {
|
||||||
|
this.cacheNextNode(cacheLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.background.nextAnimationFrameId = null;
|
||||||
|
this.scheduleAnimationFrameCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheNextNode(cacheLayer) {
|
||||||
|
let node = cacheLayer.queue.pop();
|
||||||
|
getNode(node, cacheLayer);
|
||||||
|
for (let edge of node.edges) {
|
||||||
|
cacheLayer.queue.push(edge.target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelTimeout() {
|
||||||
|
if (this.background.nextTimeoutId) {
|
||||||
|
window.clearTimeout(this.background.nextTimeoutId);
|
||||||
|
this.background.nextTimeoutId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelAnimationFrame() {
|
||||||
|
if (this.background.nextAnimationFrameId) {
|
||||||
|
window.cancelAnimationFrame(this.background.nextAnimationFrameId);
|
||||||
|
this.background.nextAnimationFrameId = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,15 +247,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 cachedNodes = getCachedNodes(token);
|
const cacheLayer = cache.getCacheLayer(token);
|
||||||
const lastNode = calculatePath(from, to, cachedNodes, token, previousWaypoints);
|
const firstNode = calculatePath(from, to, cacheLayer, previousWaypoints);
|
||||||
if (!lastNode)
|
if (!firstNode)
|
||||||
return null;
|
return null;
|
||||||
paintGriddedPathfindingDebug(lastNode, token);
|
paintGriddedPathfindingDebug(firstNode, cacheLayer.tokenData);
|
||||||
const path = [];
|
const path = [];
|
||||||
let currentNode = lastNode;
|
let currentNode = firstNode;
|
||||||
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, cacheLayer.tokenData)) {
|
||||||
// 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]);
|
||||||
@@ -125,34 +286,29 @@ export function findPath(from, to, token, previousWaypoints) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
path.push({x: currentNode.node.x, y: currentNode.node.y});
|
path.push({x: currentNode.node.x, y: currentNode.node.y});
|
||||||
currentNode = currentNode.previous;
|
currentNode = currentNode.next;
|
||||||
}
|
}
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function buildTokenData(token) {
|
||||||
* Build a cache ID based on the current token's data and then retrieve the cache to use from that
|
// Almost all the information we need is for calculating the snap point
|
||||||
*/
|
const tokenData = buildSnapPointTokenData(token);
|
||||||
function getCachedNodes(token) {
|
|
||||||
const cacheData = {};
|
|
||||||
|
|
||||||
// Different-sized tokens snap to different points on the grid,
|
// If levels is enabled, which walls matter depends on the token's elevation.
|
||||||
// so they might follow a different path to other tokens
|
// Depending on the settings in levels, the height we care about is either their
|
||||||
cacheData.tokenShape = getTokenShapeId(token);
|
// Foot height (elevation) or eye height (losHeight).
|
||||||
|
if (isModuleActive("levels")) {
|
||||||
// If levels is enabled, the token's elevation can affect which walls
|
const blockSightMovement = game.settings.get(_levelsModuleName, "blockSightMovement");
|
||||||
// they need to worry about
|
tokenData.elevation = blockSightMovement ? token.data.elevation : token.losHeight;
|
||||||
if (game.modules.get("levels")?.active) {
|
|
||||||
cacheData.elevation = token.data.elevation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheId = JSON.stringify(cacheData);
|
return tokenData;
|
||||||
return cache.getCachedNodes(cacheId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNode(pos, cachedNodes, token, initialize = true) {
|
function getNode(pos, cacheLayer, initialize = true) {
|
||||||
const node = cachedNodes[pos.y][pos.x];
|
const node = cacheLayer.nodes[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};})) {
|
||||||
@@ -161,7 +317,7 @@ function getNode(pos, cachedNodes, token, initialize = true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO Work with pixels instead of grid locations
|
// TODO Work with pixels instead of grid locations
|
||||||
if (!stepCollidesWithWall(neighborPos, pos, token)) {
|
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;
|
let edgeCost;
|
||||||
if (window.terrainRuler) {
|
if (window.terrainRuler) {
|
||||||
@@ -181,7 +337,7 @@ function getNode(pos, cachedNodes, 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, cachedNodes, token, false);
|
const neighbor = getNode(neighborPos, cacheLayer, false);
|
||||||
node.edges.push({target: neighbor, cost: edgeCost});
|
node.edges.push({target: neighbor, cost: edgeCost});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,7 +345,7 @@ function getNode(pos, cachedNodes, token, initialize = true) {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculatePath(from, to, cachedNodes, token, previousWaypoints) {
|
function calculatePath(from, to, cacheLayer, 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) {
|
||||||
@@ -202,9 +358,9 @@ function calculatePath(from, to, cachedNodes, token, previousWaypoints) {
|
|||||||
|
|
||||||
nextNodes.pushWithPriority(
|
nextNodes.pushWithPriority(
|
||||||
{
|
{
|
||||||
node: getNode(to, cachedNodes, token),
|
node: getNode(from, cacheLayer),
|
||||||
cost: startCost,
|
cost: startCost,
|
||||||
estimated: startCost + estimateCost(to, from),
|
estimated: startCost + estimateCost(from, to),
|
||||||
previous: null
|
previous: null
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -212,12 +368,12 @@ function calculatePath(from, to, cachedNodes, token, previousWaypoints) {
|
|||||||
while (nextNodes.hasNext()) {
|
while (nextNodes.hasNext()) {
|
||||||
// Get node with cheapest estimate
|
// Get node with cheapest estimate
|
||||||
const currentNode = nextNodes.pop();
|
const currentNode = nextNodes.pop();
|
||||||
if (currentNode.node.x === from.x && currentNode.node.y === from.y) {
|
if (currentNode.node.x === to.x && currentNode.node.y === to.y) {
|
||||||
return currentNode;
|
return buildPathNodes(currentNode);
|
||||||
}
|
}
|
||||||
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, cachedNodes, token);
|
const neighborNode = getNode(edge.target, cacheLayer);
|
||||||
if (previousNodes.has(neighborNode)) {
|
if (previousNodes.has(neighborNode)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -225,7 +381,7 @@ function calculatePath(from, to, cachedNodes, token, previousWaypoints) {
|
|||||||
const neighbor = {
|
const neighbor = {
|
||||||
node: neighborNode,
|
node: neighborNode,
|
||||||
cost: currentNode.cost + edge.cost,
|
cost: currentNode.cost + edge.cost,
|
||||||
estimated: currentNode.cost + edge.cost + estimateCost(neighborNode, from),
|
estimated: currentNode.cost + edge.cost + estimateCost(neighborNode, to),
|
||||||
previous: currentNode
|
previous: currentNode
|
||||||
};
|
};
|
||||||
nextNodes.pushWithPriority(neighbor);
|
nextNodes.pushWithPriority(neighbor);
|
||||||
@@ -233,6 +389,25 @@ function calculatePath(from, to, cachedNodes, token, previousWaypoints) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Now we've found the path, we know the final node, and each node links to the previous one.
|
||||||
|
* Reverse this list and return the first node in the path, with each node linking to the next
|
||||||
|
*/
|
||||||
|
function buildPathNodes(lastNode) {
|
||||||
|
let currentNode = lastNode;
|
||||||
|
let previousNode = null;
|
||||||
|
while (currentNode) {
|
||||||
|
const pathNode = {
|
||||||
|
node: currentNode.node,
|
||||||
|
cost: currentNode.cost,
|
||||||
|
next: previousNode
|
||||||
|
}
|
||||||
|
previousNode = pathNode;
|
||||||
|
currentNode = currentNode.previous;
|
||||||
|
}
|
||||||
|
return previousNode;
|
||||||
|
}
|
||||||
|
|
||||||
function calcNoDiagonals(waypoints) {
|
function calcNoDiagonals(waypoints) {
|
||||||
let diagonals = 0;
|
let diagonals = 0;
|
||||||
for (const [p1, p2] of iterPairs(waypoints)) {
|
for (const [p1, p2] of iterPairs(waypoints)) {
|
||||||
@@ -251,10 +426,16 @@ function estimateCost(pos, target) {
|
|||||||
return Math.max(distX, distY) + (use5105 ? Math.min(distX, distY) * 0.5 : 0);
|
return Math.max(distX, distY) + (use5105 ? Math.min(distX, distY) * 0.5 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stepCollidesWithWall(from, to, token) {
|
function stepCollidesWithWall(from, to, tokenData) {
|
||||||
const stepStart = getSnapPointForTokenObj(getPixelsFromGridPositionObj(from), token);
|
const stepStart = getSnapPointForTokenDataObj(getPixelsFromGridPositionObj(from), tokenData);
|
||||||
const stepEnd = getSnapPointForTokenObj(getPixelsFromGridPositionObj(to), token);
|
const stepEnd = getSnapPointForTokenDataObj(getPixelsFromGridPositionObj(to), tokenData);
|
||||||
return canvas.walls.checkCollision(new Ray(stepStart, stepEnd));
|
if (isModuleActive("levels")) {
|
||||||
|
stepStart.z = tokenData.elevation;
|
||||||
|
stepEnd.z = tokenData.elevation;
|
||||||
|
return _levels.testCollision(stepStart, stepEnd, "collision")
|
||||||
|
} else {
|
||||||
|
return canvas.walls.checkCollision(new Ray(stepStart, stepEnd));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function wipePathfindingCache() {
|
export function wipePathfindingCache() {
|
||||||
@@ -272,20 +453,26 @@ export function initializePathfinding() {
|
|||||||
gridHeight = Math.ceil(canvas.dimensions.height / canvas.grid.h);
|
gridHeight = Math.ceil(canvas.dimensions.height / canvas.grid.h);
|
||||||
}
|
}
|
||||||
|
|
||||||
function paintGriddedPathfindingDebug(lastNode, token) {
|
export function startBackgroundCaching(token) {
|
||||||
|
if (game.user.isGM || game.settings.get(settingsKey, "allowPathfinding")) {
|
||||||
|
cache.startBackgroundCaching(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintGriddedPathfindingDebug(firstNode, tokenData) {
|
||||||
if (!CONFIG.debug.dragRuler)
|
if (!CONFIG.debug.dragRuler)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
debugGraphics.removeChildren().forEach(c => c.destroy());
|
debugGraphics.removeChildren().forEach(c => c.destroy());
|
||||||
let currentNode = lastNode;
|
let currentNode = firstNode;
|
||||||
while (currentNode) {
|
while (currentNode) {
|
||||||
let text = new PIXI.Text(currentNode.cost.toFixed(1));
|
let text = new PIXI.Text(currentNode.cost.toFixed(1));
|
||||||
let pixels = getSnapPointForTokenObj(getPixelsFromGridPositionObj(currentNode.node), token);
|
let pixels = getSnapPointForTokenDataObj(getPixelsFromGridPositionObj(currentNode.node), tokenData);
|
||||||
text.anchor.set(0.5, 1.0);
|
text.anchor.set(0.5, 1.0);
|
||||||
text.x = pixels.x;
|
text.x = pixels.x;
|
||||||
text.y = pixels.y;
|
text.y = pixels.y;
|
||||||
debugGraphics.addChild(text);
|
debugGraphics.addChild(text);
|
||||||
currentNode = currentNode.previous;
|
currentNode = currentNode.next;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+47
-36
@@ -1,5 +1,6 @@
|
|||||||
import {getPixelsFromGridPosition} from "./foundry_fixes.js"
|
import {getPixelsFromGridPosition} from "./foundry_fixes.js"
|
||||||
import { disableSnap } from "./keybindings.js";
|
import {findVertexSnapPoint} from "./hex_support.js";
|
||||||
|
import {disableSnap} from "./keybindings.js";
|
||||||
|
|
||||||
export function* zip(it1, it2) {
|
export function* zip(it1, it2) {
|
||||||
for (let i = 0;i < Math.min(it1.length, it2.length);i++) {
|
for (let i = 0;i < Math.min(it1.length, it2.length);i++) {
|
||||||
@@ -25,14 +26,38 @@ export function sum(arr) {
|
|||||||
return arr.reduce((a, b) => a + b, 0);
|
return arr.reduce((a, b) => a + b, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildSnapPointTokenData(token) {
|
||||||
|
const tokenData = {
|
||||||
|
width: token.data.width,
|
||||||
|
height: token.data.height
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isModuleActive("hex-size-support")) {
|
||||||
|
tokenData.hexSizeSupport = {};
|
||||||
|
tokenData.hexSizeSupport.altSnappingFlag = CONFIG.hexSizeSupport.getAltSnappingFlag(token);
|
||||||
|
tokenData.hexSizeSupport.altOrientationFlag = CONFIG.hexSizeSupport.getAltOrientationFlag(token);
|
||||||
|
tokenData.hexSizeSupport.borderSize = token.document.getFlag("hex-size-support", "borderSize");
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenData;
|
||||||
|
}
|
||||||
|
|
||||||
export function getSnapPointForToken(x, y, token) {
|
export function getSnapPointForToken(x, y, token) {
|
||||||
|
return getSnapPointForTokenData(x, y, buildSnapPointTokenData(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSnapPointForTokenDataObj(pos, tokenData) {
|
||||||
|
return getSnapPointForTokenData(pos.x, pos.y, tokenData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSnapPointForTokenData(x, y, tokenData) {
|
||||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||||
return new PIXI.Point(x, y);
|
return new PIXI.Point(x, y);
|
||||||
}
|
}
|
||||||
if (canvas.grid.isHex) {
|
if (canvas.grid.isHex) {
|
||||||
if (game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) {
|
if (tokenData.hexSizeSupport?.altSnappingFlag) {
|
||||||
if (token.document.getFlag("hex-size-support", "borderSize") % 2 === 0) {
|
if (tokenData.hexSizeSupport.borderSize % 2 === 0) {
|
||||||
const snapPoint = CONFIG.hexSizeSupport.findVertexSnapPoint(x, y, token, canvas.grid.grid)
|
const snapPoint = findVertexSnapPoint(x, y, tokenData.hexSizeSupport.altOrientationFlag);
|
||||||
return new PIXI.Point(snapPoint.x, snapPoint.y)
|
return new PIXI.Point(snapPoint.x, snapPoint.y)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -46,38 +71,38 @@ export function getSnapPointForToken(x, y, token) {
|
|||||||
|
|
||||||
const [topLeftX, topLeftY] = canvas.grid.getTopLeft(x, y);
|
const [topLeftX, topLeftY] = canvas.grid.getTopLeft(x, y);
|
||||||
let cellX, cellY;
|
let cellX, cellY;
|
||||||
if (token.data.width % 2 === 0)
|
if (tokenData.width % 2 === 0)
|
||||||
cellX = x - canvas.grid.h / 2;
|
cellX = x - canvas.grid.h / 2;
|
||||||
else
|
else
|
||||||
cellX = x;
|
cellX = x;
|
||||||
if (token.data.height % 2 === 0)
|
if (tokenData.height % 2 === 0)
|
||||||
cellY = y - canvas.grid.h / 2;
|
cellY = y - canvas.grid.h / 2;
|
||||||
else
|
else
|
||||||
cellY = y;
|
cellY = y;
|
||||||
const [centerX, centerY] = canvas.grid.getCenter(cellX, cellY);
|
const [centerX, centerY] = canvas.grid.getCenter(cellX, cellY);
|
||||||
let snapX, snapY;
|
let snapX, snapY;
|
||||||
// Tiny tokens can snap to the cells corners
|
// Tiny tokens can snap to the cells corners
|
||||||
if (token.data.width <= 0.5) {
|
if (tokenData.width <= 0.5) {
|
||||||
const offsetX = x - topLeftX;
|
const offsetX = x - topLeftX;
|
||||||
const subGridWidth = Math.floor(canvas.grid.w / 2);
|
const subGridWidth = Math.floor(canvas.grid.w / 2);
|
||||||
const subGridPosX = Math.floor(offsetX / subGridWidth);
|
const subGridPosX = Math.floor(offsetX / subGridWidth);
|
||||||
snapX = topLeftX + (subGridPosX + 0.5) * subGridWidth;
|
snapX = topLeftX + (subGridPosX + 0.5) * subGridWidth;
|
||||||
}
|
}
|
||||||
// Tokens with odd multipliers (1x1, 3x3, ...) and tokens smaller than 1x1 but bigger than 0.5x0.5 snap to the center of the grid cell
|
// Tokens with odd multipliers (1x1, 3x3, ...) and tokens smaller than 1x1 but bigger than 0.5x0.5 snap to the center of the grid cell
|
||||||
else if (Math.round(token.data.width) % 2 === 1 || token.data.width < 1) {
|
else if (Math.round(tokenData.width) % 2 === 1 || tokenData.width < 1) {
|
||||||
snapX = centerX;
|
snapX = centerX;
|
||||||
}
|
}
|
||||||
// All remaining tokens (those with even or fractional multipliers on square grids) snap to the intersection points of the grid
|
// All remaining tokens (those with even or fractional multipliers on square grids) snap to the intersection points of the grid
|
||||||
else {
|
else {
|
||||||
snapX = centerX + canvas.grid.w / 2;
|
snapX = centerX + canvas.grid.w / 2;
|
||||||
}
|
}
|
||||||
if (token.data.height <= 0.5) {
|
if (tokenData.height <= 0.5) {
|
||||||
const offsetY = y - topLeftY;
|
const offsetY = y - topLeftY;
|
||||||
const subGridHeight = Math.floor(canvas.grid.h / 2);
|
const subGridHeight = Math.floor(canvas.grid.h / 2);
|
||||||
const subGridPosY = Math.floor(offsetY / subGridHeight);
|
const subGridPosY = Math.floor(offsetY / subGridHeight);
|
||||||
snapY = topLeftY + (subGridPosY + 0.5) * subGridHeight;
|
snapY = topLeftY + (subGridPosY + 0.5) * subGridHeight;
|
||||||
}
|
}
|
||||||
else if (Math.round(token.data.height) % 2 === 1 || token.data.height < 1) {
|
else if (Math.round(tokenData.height) % 2 === 1 || tokenData.height < 1) {
|
||||||
snapY = centerY;
|
snapY = centerY;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -90,12 +115,19 @@ export function getSnapPointForTokenObj(pos, token) {
|
|||||||
return getSnapPointForToken(pos.x, pos.y, token);
|
return getSnapPointForToken(pos.x, pos.y, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSnapPointForMeasuredTemplate(x, y) {
|
||||||
|
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||||
|
return new PIXI.Point(x, y);
|
||||||
|
}
|
||||||
|
return canvas.grid.grid.getSnappedPosition(x, y, canvas.templates.gridPosition);
|
||||||
|
}
|
||||||
|
|
||||||
export function getSnapPointForEntity(x, y, entity) {
|
export function getSnapPointForEntity(x, y, entity) {
|
||||||
const isToken = entity instanceof Token;
|
const isToken = entity instanceof Token;
|
||||||
if (isToken)
|
if (isToken)
|
||||||
return getSnapPointForToken(x, y, entity);
|
return getSnapPointForToken(x, y, entity);
|
||||||
else
|
else
|
||||||
return canvas.grid.grid.getSnappedPosition(x, y, canvas.templates.gridPosition);
|
return getSnapPointForMeasuredTemplate(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function highlightTokenShape(position, shape, color, alpha) {
|
export function highlightTokenShape(position, shape, color, alpha) {
|
||||||
@@ -134,31 +166,6 @@ export function getAreaFromPositionAndShape(position, shape) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function getTokenShapeId(token) {
|
|
||||||
if (token.scene.data.gridType === CONST.GRID_TYPES.GRIDLESS) {
|
|
||||||
return "1";
|
|
||||||
}
|
|
||||||
else if (token.scene.data.gridType === CONST.GRID_TYPES.SQUARE) {
|
|
||||||
return `${token.data.width}x${token.data.height}`;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Hex grids
|
|
||||||
if (game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) {
|
|
||||||
const borderSize = token.data.flags["hex-size-support"].borderSize;
|
|
||||||
|
|
||||||
let inversionFlag = "";
|
|
||||||
if (Boolean(CONFIG.hexSizeSupport.getAltOrientationFlag(token)) !== canvas.grid.grid.options.columns)
|
|
||||||
inversionFlag = "i";
|
|
||||||
|
|
||||||
return `${borderSize}${inversionFlag}`;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return "1";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTokenShape(token) {
|
export function getTokenShape(token) {
|
||||||
if (token.scene.data.gridType === CONST.GRID_TYPES.GRIDLESS) {
|
if (token.scene.data.gridType === CONST.GRID_TYPES.GRIDLESS) {
|
||||||
return [{x: 0, y: 0}]
|
return [{x: 0, y: 0}]
|
||||||
@@ -281,3 +288,7 @@ export function early_isGM() {
|
|||||||
const gmLevel = CONST.USER_ROLES.ASSISTANT;
|
const gmLevel = CONST.USER_ROLES.ASSISTANT;
|
||||||
return level >= gmLevel;
|
return level >= gmLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isModuleActive(moduleName) {
|
||||||
|
return game.modules.get(moduleName)?.active
|
||||||
|
}
|
||||||
|
|||||||
+2
-2
@@ -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.5",
|
"version": "1.12.6",
|
||||||
"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.5/drag-ruler-1.12.5.zip",
|
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/releases/download/v1.12.6/drag-ruler-1.12.6.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",
|
||||||
|
|||||||
Generated
+1
-1
@@ -26,7 +26,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridless-pathfinding"
|
name = "gridless-pathfinding"
|
||||||
version = "1.12.5"
|
version = "1.12.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gridless-pathfinding"
|
name = "gridless-pathfinding"
|
||||||
version = "1.12.5"
|
version = "1.12.6"
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user