Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c08d65ffad |
@@ -1,4 +0,0 @@
|
|||||||
printWidth: 100
|
|
||||||
trailingComma: "all"
|
|
||||||
bracketSpacing: false
|
|
||||||
arrowParens: "avoid"
|
|
||||||
@@ -1,20 +1,3 @@
|
|||||||
## 1.13.0
|
|
||||||
### Breaking changes
|
|
||||||
- Drag Ruler's pathfinder has been extracted into a dedicated library module. If you'd like to continue to use Drag Ruler's pathfinding feature, please install [routinglib](https://foundryvtt.com/packages/routinglib) alongside Drag Ruler.
|
|
||||||
- Drag Ruler's API no longer supports the function `getCostForStep`. Instead, module authors are asked to use [Enhanced Terrain Layer's game system integration API](https://github.com/ironmonk88/enhanced-terrain-layer/blob/main/README.md#integrating-game-system-rules) to introduce game system specific terrain rules.
|
|
||||||
|
|
||||||
### New features
|
|
||||||
- Drag Ruler's pathfinding will now be running as a background task. This means that Foundry will no longer freeze while Drag Ruler is calculating a path.
|
|
||||||
- Drag Ruler's pathfinding will now take difficult terrain into account on griddes scenes
|
|
||||||
|
|
||||||
### Bug fixes
|
|
||||||
- Fixed a bug that would cause gridless snapping to snap slightly below the allowed range
|
|
||||||
|
|
||||||
### Compatibility
|
|
||||||
- Drag Ruler is now compatible with Foundry VTT v10
|
|
||||||
- Drag Ruler's compatibility with the Wall Height module is restored
|
|
||||||
|
|
||||||
|
|
||||||
## 1.12.8
|
## 1.12.8
|
||||||
### Bugfixes
|
### Bugfixes
|
||||||
- Fixed a bug that could cause grid cells to be highlighted in the wrong color
|
- Fixed a bug that could cause grid cells to be highlighted in the wrong color
|
||||||
|
|||||||
@@ -32,16 +32,6 @@ During combat, Drag Ruler will remember the path a token has taken during it's t
|
|||||||

|

|
||||||
|
|
||||||
|
|
||||||
## Pathfinding
|
|
||||||
**To use pathfinding you must install the [routinglib](https://foundryvtt.com/packages/routinglib) module**
|
|
||||||
|
|
||||||
When routinglib is installed, Drag Ruler can automatically place waypoints to walk around walls and terrain to reach the destination with the shortest possible movement. Pathfinding can be activated using a configurable key. Alternatively, Drag Ruler can be configured to always use pathfinding when a token is being dragged.
|
|
||||||
|
|
||||||
Pathfinding is restricted to GM users by default, since the pathfinding algorithm can create ways that lead through unexplored fog of war. If you want to allow your players to use Drag Ruler's pathfinding functionality, you need to enable the associated setting in Drag Ruler's module settings.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
## Game systems with Drag Ruler integration
|
## Game systems with Drag Ruler integration
|
||||||
Drag Ruler will work with all Foundry VTT game systems. However, some game systems offer a special integration via the [Drag Ruler API](#api), that allows Drag Ruler to take the rules of the game system into account when dispaying speeds (such as weight carried or conditions that apply to the character), offering a smoother experience. While some game systems offer this integration natively, for other game systems there are modules providing the integration. If the integration is provided via a module you need to install and activate both Drag Ruler and the integration module to benefit from the integration.
|
Drag Ruler will work with all Foundry VTT game systems. However, some game systems offer a special integration via the [Drag Ruler API](#api), that allows Drag Ruler to take the rules of the game system into account when dispaying speeds (such as weight carried or conditions that apply to the character), offering a smoother experience. While some game systems offer this integration natively, for other game systems there are modules providing the integration. If the integration is provided via a module you need to install and activate both Drag Ruler and the integration module to benefit from the integration.
|
||||||
|
|
||||||
@@ -49,7 +39,6 @@ The game systems that offer Drag Ruler integration are:
|
|||||||
- Cypher System (starting with version 1.13.0)
|
- Cypher System (starting with version 1.13.0)
|
||||||
- DnD5e (via the module [DnD5e Drag Ruler Integration](https://foundryvtt.com/packages/elevation-drag-ruler))
|
- DnD5e (via the module [DnD5e Drag Ruler Integration](https://foundryvtt.com/packages/elevation-drag-ruler))
|
||||||
- GURPS 4th Edition Game Aid (Unofficial) (starting with version 0.9.1)
|
- GURPS 4th Edition Game Aid (Unofficial) (starting with version 0.9.1)
|
||||||
- Hackmaster (starting with version 0.2.11)
|
|
||||||
- Ironclaw Second Edition (starting with version 0.2.2)
|
- Ironclaw Second Edition (starting with version 0.2.2)
|
||||||
- Lancer (via the module [Lancer Speed Provider](https://foundryvtt.com/packages/lancer-speed-provider))
|
- Lancer (via the module [Lancer Speed Provider](https://foundryvtt.com/packages/lancer-speed-provider))
|
||||||
- Level Up: Advanced 5th Edition (Official) (via the module [A5E Drag Ruler Integration](https://foundryvtt.com/packages/a5edragruler))
|
- Level Up: Advanced 5th Edition (Official) (via the module [A5E Drag Ruler Integration](https://foundryvtt.com/packages/a5edragruler))
|
||||||
@@ -63,7 +52,6 @@ The game systems that offer Drag Ruler integration are:
|
|||||||
- Tagmar RPG (starting with version 1.1.4)
|
- Tagmar RPG (starting with version 1.1.4)
|
||||||
- TheWitcherTRPG (starting with version 0.0.62)
|
- TheWitcherTRPG (starting with version 0.0.62)
|
||||||
- Tormenta20 (starting with version 1.1.37)
|
- Tormenta20 (starting with version 1.1.37)
|
||||||
- The Dark Eye 5 / Das Schwarze Auge 5 (via the module [TDE5/DSA5 Drag Ruler Integration](https://foundryvtt.com/packages/dsa5-drag-ruler))
|
|
||||||
- Shadow of the Demon Lord (starting with version 1.7.15)
|
- Shadow of the Demon Lord (starting with version 1.7.15)
|
||||||
- Wasteland Ventures (starting with version 0.1.0)
|
- Wasteland Ventures (starting with version 0.1.0)
|
||||||
- WWII:OWB (starting with version 1.0.4)
|
- WWII:OWB (starting with version 1.0.4)
|
||||||
|
|||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import PurePath, Path
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
wasm_pack = Path("~/.cargo/bin/wasm-pack").expanduser()
|
||||||
|
|
||||||
|
root_files = ["module.json", "README.md", "CHANGELOG.md", "LICENSE"]
|
||||||
|
wasm_files = ["gridless_pathfinding_bg.wasm", "gridless_pathfinding.js"]
|
||||||
|
output_dir = Path("artifact")
|
||||||
|
copy_everything_directories = ["js", "lang", "templates"]
|
||||||
|
wasm_dir = Path("wasm")
|
||||||
|
root_dir = Path(".")
|
||||||
|
rust_dir = Path("rust")
|
||||||
|
build_dir_tmp = tempfile.TemporaryDirectory()
|
||||||
|
build_dir = Path(build_dir_tmp.name)
|
||||||
|
|
||||||
|
with open("module.json", "r") as file:
|
||||||
|
manifest = json.load(file)
|
||||||
|
|
||||||
|
zip_root = PurePath(f'{manifest["name"]}')
|
||||||
|
|
||||||
|
filename = f'{manifest["name"]}-{manifest["version"]}.zip'
|
||||||
|
|
||||||
|
result = subprocess.run([wasm_pack, "build", "--target", "web", "--out-dir", build_dir, root_dir / rust_dir])
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise Exception("Wasm build failed")
|
||||||
|
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def write_directory(archive, d):
|
||||||
|
for f in (root_dir / d).iterdir():
|
||||||
|
if f.is_dir():
|
||||||
|
write_directory(archive, f)
|
||||||
|
else:
|
||||||
|
assert(f.is_file())
|
||||||
|
archive.write(f, arcname=zip_root / d / f.name)
|
||||||
|
|
||||||
|
with zipfile.ZipFile(output_dir / filename, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
|
||||||
|
for f in root_files:
|
||||||
|
archive.write(root_dir / f, arcname=zip_root / f)
|
||||||
|
for d in copy_everything_directories:
|
||||||
|
write_directory(archive, d)
|
||||||
|
for f in wasm_files:
|
||||||
|
archive.write(build_dir / f, arcname=zip_root / wasm_dir / f)
|
||||||
|
|
||||||
|
print(f"Successfully built {output_dir / filename}")
|
||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
root_dir = Path(".")
|
||||||
|
wasm_dir = root_dir / Path("wasm")
|
||||||
|
rust_dir = root_dir / Path("rust")
|
||||||
|
|
||||||
|
debug = " --debug" if len(sys.argv) >= 2 and sys.argv[1] == "--debug" else ""
|
||||||
|
|
||||||
|
result = subprocess.run(["cargo", "watch", "-C" , rust_dir, "-s", f"wasm-pack build --target web --out-dir {wasm_dir.resolve()}{debug}"])
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
cargo install cargo-watch
|
||||||
|
cargo install wasm-pack
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import {measureDistances} from "./compatibility.js";
|
||||||
|
import {getMovementHistory} from "./movement_tracking.js";
|
||||||
|
import {GenericSpeedProvider, SpeedProvider} from "./speed_provider.js"
|
||||||
|
import {settingsKey} from "./settings.js"
|
||||||
|
import {getAreaFromPositionAndShape, getTokenShape} from "./util.js";
|
||||||
|
|
||||||
|
export const availableSpeedProviders = {}
|
||||||
|
export let currentSpeedProvider = undefined
|
||||||
|
|
||||||
|
function register(module, type, speedProvider) {
|
||||||
|
const id = `${type}.${module.id}`
|
||||||
|
let providerInstance
|
||||||
|
if (speedProvider.prototype instanceof SpeedProvider) {
|
||||||
|
providerInstance = new speedProvider(id)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.warn(`Drag Ruler | The ${type} '${module.id}' uses the old, deprecated version of the Drag Ruler API. ` +
|
||||||
|
"That old API will be removed in a future Drag Ruler version. " +
|
||||||
|
`Please update the ${type} ${module.id} to stay compatible with future Drag Ruler versions.`);
|
||||||
|
speedProvider.id = id
|
||||||
|
speedProvider.usesRuler = () => true
|
||||||
|
providerInstance = speedProvider
|
||||||
|
}
|
||||||
|
setupProvider(providerInstance)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupProvider(speedProvider) {
|
||||||
|
if (speedProvider instanceof SpeedProvider) {
|
||||||
|
const unreachableColor = {id: "unreachable", default: speedProvider.defaultUnreachableColor, name: "drag-ruler.settings.speedProviderSettings.color.unreachable.name"}
|
||||||
|
for (const color of speedProvider.colors.concat([unreachableColor])) {
|
||||||
|
game.settings.register(settingsKey, `speedProviders.${speedProvider.id}.color.${color.id}`, {
|
||||||
|
config: false,
|
||||||
|
scope: "client",
|
||||||
|
type: Number,
|
||||||
|
default: color.default,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for (const setting of speedProvider.settings) {
|
||||||
|
setting.config = false
|
||||||
|
game.settings.register(settingsKey, `speedProviders.${speedProvider.id}.setting.${setting.id}`, setting)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
availableSpeedProviders[speedProvider.id] = speedProvider
|
||||||
|
game.settings.settings.get("drag-ruler.speedProvider").default = getDefaultSpeedProvider()
|
||||||
|
updateSpeedProvider()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDefaultSpeedProvider() {
|
||||||
|
const providerIds = Object.keys(availableSpeedProviders)
|
||||||
|
// Game systems take the highest precedence for the being the default
|
||||||
|
const gameSystem = providerIds.find(key => key.startsWith("system."))
|
||||||
|
if (gameSystem)
|
||||||
|
return gameSystem
|
||||||
|
|
||||||
|
// If no game system is registered modules are next up.
|
||||||
|
// For lack of a method to select the best module we're just falling back to taking the next best module
|
||||||
|
// settingKeys should always be sorted the same way so this should achive a stable default
|
||||||
|
const module = providerIds.find(key => key.startsWith("module."))
|
||||||
|
if (module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
// If neither a game system or a module is found fall back to the native implementation
|
||||||
|
return providerIds[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateSpeedProvider() {
|
||||||
|
// If the configured provider is registered use that one. If not use the default provider
|
||||||
|
const configuredProvider = game.settings.get("drag-ruler", "speedProvider")
|
||||||
|
currentSpeedProvider = availableSpeedProviders[configuredProvider] ?? availableSpeedProviders[game.settings.settings.get("drag-ruler.speedProvider").default]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initApi() {
|
||||||
|
const genericSpeedProviderInstance = new GenericSpeedProvider("native")
|
||||||
|
setupProvider(genericSpeedProviderInstance)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRangesFromSpeedProvider(token) {
|
||||||
|
try {
|
||||||
|
if (currentSpeedProvider instanceof Function)
|
||||||
|
return currentSpeedProvider(token, 0x00FF00)
|
||||||
|
const ranges = currentSpeedProvider.getRanges(token)
|
||||||
|
for (const range of ranges) {
|
||||||
|
range.color = game.settings.get(settingsKey, `speedProviders.${currentSpeedProvider.id}.color.${range.color}`)
|
||||||
|
}
|
||||||
|
return ranges
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUnreachableColorFromSpeedProvider() {
|
||||||
|
if (currentSpeedProvider instanceof Function)
|
||||||
|
return 0xFF0000
|
||||||
|
try {
|
||||||
|
return game.settings.get(settingsKey, `speedProviders.${currentSpeedProvider.id}.color.unreachable`)
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
return 0xFF0000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCostFromSpeedProvider(token, area, options) {
|
||||||
|
try {
|
||||||
|
if (currentSpeedProvider instanceof Function) {
|
||||||
|
return SpeedProvider.prototype.getCostForStep.call(undefined, token, area, options);
|
||||||
|
}
|
||||||
|
return currentSpeedProvider.getCostForStep(token, area, options);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getColorForDistanceAndToken(distance, token, ranges=null) {
|
||||||
|
if (!ranges) {
|
||||||
|
ranges = getRangesFromSpeedProvider(token);
|
||||||
|
}
|
||||||
|
if (ranges.length === 0)
|
||||||
|
return null;
|
||||||
|
const currentRange = ranges.reduce((minRange, currentRange) => {
|
||||||
|
if (distance <= currentRange.range && currentRange.range < minRange.range)
|
||||||
|
return currentRange;
|
||||||
|
return minRange;
|
||||||
|
}, {range: Infinity, color: getUnreachableColorFromSpeedProvider()});
|
||||||
|
return currentRange.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMovedDistanceFromToken(token) {
|
||||||
|
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active;
|
||||||
|
const history = getMovementHistory(token);
|
||||||
|
const segments = Ruler.dragRulerGetRaysFromWaypoints(history, {x: token.x, y: token.y}).map(ray => {return {ray}});
|
||||||
|
const shape = getTokenShape(token);
|
||||||
|
const distances = measureDistances(segments, token, shape, {enableTerrainRuler: terrainRulerAvailable});
|
||||||
|
// Sum up the distances
|
||||||
|
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)
|
||||||
|
// If it doesn't the calling module did something wrong. Log a warning and ignore this module
|
||||||
|
if (!module) {
|
||||||
|
console.warn(
|
||||||
|
`Drag Ruler | A module tried to register with the id "${moduleId}". However no active module with this id was found.` +
|
||||||
|
"This api registration call was ignored. " +
|
||||||
|
"If you are the author of that module please check that the id passed to `registerModule` matches the id in your manifest exactly." +
|
||||||
|
"If this call was made form a game system instead of a module please use `registerSystem` instead.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Using Drag Ruler's id is not allowed
|
||||||
|
if (moduleId === "drag-ruler") {
|
||||||
|
console.warn(
|
||||||
|
`Drag Ruler | A module tried to register with the id "${moduleId}", which is not allowed. This api registration call was ignored. ` +
|
||||||
|
"If you're the author of the module please use the id of your own module as it's specified in your manifest to register to this api. " +
|
||||||
|
"If this call was made form a game system instead of a module please use `registerSystem` instead."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
register(module, "module", speedProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerSystem(systemId, speedProvider) {
|
||||||
|
const system = game.system
|
||||||
|
// If the current system id doesn't match the provided id something went wrong. Log a warning and ignore this module
|
||||||
|
if (system.id != systemId) {
|
||||||
|
console.warn(
|
||||||
|
`Drag Ruler | A system tried to register with the id "${systemId}". However the active system has a different id.` +
|
||||||
|
"This api registration call was ignored. " +
|
||||||
|
"If you are the author of that system please check that the id passed to `registerSystem` matches the id in your manifest exactly." +
|
||||||
|
"If this call was made form a module instead of a game system please use `registerModule` instead.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
register(system, "system", speedProvider)
|
||||||
|
}
|
||||||
@@ -3,24 +3,19 @@ import {settingsKey} from "./settings.js";
|
|||||||
import {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)
|
||||||
return {x: token.x + tokenCenterOffset.x, y: token.y + tokenCenterOffset.y};
|
return {x: token.x + tokenCenterOffset.x, y: token.y + tokenCenterOffset.y}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function highlightMeasurementTerrainRuler(
|
export function highlightMeasurementTerrainRuler(ray, startDistance, tokenShape=[{x: 0, y: 0}], alpha=1) {
|
||||||
ray,
|
|
||||||
startDistance,
|
|
||||||
tokenShape = [{x: 0, y: 0}],
|
|
||||||
alpha = 1,
|
|
||||||
) {
|
|
||||||
for (const space of ray.terrainRulerVisitedSpaces.reverse()) {
|
for (const space of ray.terrainRulerVisitedSpaces.reverse()) {
|
||||||
const color = this.dragRulerGetColorForDistance(startDistance + space.distance);
|
const color = this.dragRulerGetColorForDistance(startDistance + space.distance);
|
||||||
highlightTokenShape.call(this, space, tokenShape, color, alpha);
|
highlightTokenShape.call(this, space, tokenShape, color, alpha)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function measureDistances(segments, entity, shape, options = {}) {
|
export function measureDistances(segments, entity, shape, options={}) {
|
||||||
const opts = duplicate(options);
|
const opts = duplicate(options)
|
||||||
if (canvas.grid.diagonalRule === "EUCL") {
|
if (canvas.grid.diagonalRule === "EUCL") {
|
||||||
opts.ignoreGrid = true;
|
opts.ignoreGrid = true;
|
||||||
opts.gridSpaes = false;
|
opts.gridSpaes = false;
|
||||||
@@ -30,23 +25,17 @@ export function measureDistances(segments, entity, shape, options = {}) {
|
|||||||
const firstNewSegmentIndex = segments.findIndex(segment => !segment.ray.dragRulerVisitedSpaces);
|
const firstNewSegmentIndex = segments.findIndex(segment => !segment.ray.dragRulerVisitedSpaces);
|
||||||
const previousSegments = segments.slice(0, firstNewSegmentIndex);
|
const previousSegments = segments.slice(0, firstNewSegmentIndex);
|
||||||
const newSegments = segments.slice(firstNewSegmentIndex);
|
const newSegments = segments.slice(firstNewSegmentIndex);
|
||||||
const distances = previousSegments.map(
|
const distances = previousSegments.map(segment => segment.ray.dragRulerVisitedSpaces[segment.ray.dragRulerVisitedSpaces.length - 1].distance);
|
||||||
segment =>
|
previousSegments.forEach(segment => segment.ray.terrainRulerVisitedSpaces = duplicate(segment.ray.dragRulerVisitedSpaces));
|
||||||
segment.ray.dragRulerVisitedSpaces[segment.ray.dragRulerVisitedSpaces.length - 1].distance,
|
|
||||||
);
|
|
||||||
previousSegments.forEach(
|
|
||||||
segment =>
|
|
||||||
(segment.ray.terrainRulerVisitedSpaces = duplicate(segment.ray.dragRulerVisitedSpaces)),
|
|
||||||
);
|
|
||||||
opts.costFunction = buildCostFunction(entity, shape);
|
opts.costFunction = buildCostFunction(entity, shape);
|
||||||
if (previousSegments.length > 0)
|
if (previousSegments.length > 0)
|
||||||
opts.terrainRulerInitialState =
|
opts.terrainRulerInitialState = previousSegments[previousSegments.length - 1].ray.dragRulerFinalState;
|
||||||
previousSegments[previousSegments.length - 1].ray.dragRulerFinalState;
|
|
||||||
return distances.concat(terrainRuler.measureDistances(newSegments, opts));
|
return distances.concat(terrainRuler.measureDistances(newSegments, opts));
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
// If another module wants to enable grid measurements but disable grid highlighting,
|
// If another module wants to enable grid measurements but disable grid highlighting,
|
||||||
// manually set the *duplicate* option's gridSpaces value to true for the Foundry logic to work properly
|
// manually set the *duplicate* option's gridSpaces value to true for the Foundry logic to work properly
|
||||||
if (!opts.ignoreGrid) {
|
if(!opts.ignoreGrid) {
|
||||||
opts.gridSpaces = true;
|
opts.gridSpaces = true;
|
||||||
}
|
}
|
||||||
return canvas.grid.measureDistances(segments, opts);
|
return canvas.grid.measureDistances(segments, opts);
|
||||||
@@ -55,31 +44,23 @@ export function measureDistances(segments, entity, shape, options = {}) {
|
|||||||
|
|
||||||
export function checkDependencies() {
|
export function checkDependencies() {
|
||||||
if (!game.modules.get("socketlib")?.active) {
|
if (!game.modules.get("socketlib")?.active) {
|
||||||
console.error(
|
console.error("Drag Ruler | The `socketlib` module isn't enabled, but it's required for Drag Ruler to operate properly.");
|
||||||
"Drag Ruler | The `socketlib` module isn't enabled, but it's required for Drag Ruler to operate properly.",
|
|
||||||
);
|
|
||||||
if (game.user.isGM) {
|
if (game.user.isGM) {
|
||||||
new Dialog({
|
new Dialog({
|
||||||
title: game.i18n.localize("drag-ruler.dependencies.socketlib.title"),
|
title: game.i18n.localize("drag-ruler.dependencies.socketlib.title"),
|
||||||
content: `<h2>${game.i18n.localize(
|
content: `<h2>${game.i18n.localize("drag-ruler.dependencies.socketlib.title")}</h2><p>${game.i18n.localize("drag-ruler.dependencies.socketlib.text")}</p>`,
|
||||||
"drag-ruler.dependencies.socketlib.title",
|
|
||||||
)}</h2><p>${game.i18n.localize("drag-ruler.dependencies.socketlib.text")}</p>`,
|
|
||||||
buttons: {
|
buttons: {
|
||||||
ok: {
|
ok: {
|
||||||
icon: '<i class="fas fa-check"></i>',
|
icon: '<i class="fas fa-check"></i>',
|
||||||
label: game.i18n.localize("drag-ruler.dependencies.ok"),
|
label: game.i18n.localize("drag-ruler.dependencies.ok")
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
}).render(true);
|
}).render(true);
|
||||||
}
|
}
|
||||||
} else if (
|
}
|
||||||
!game.modules.get("terrain-ruler")?.active &&
|
else if (!game.modules.get("terrain-ruler")?.active && game.user.isGM && !game.settings.get(settingsKey, "neverShowTerrainRulerHint")) {
|
||||||
game.user.isGM &&
|
|
||||||
!game.settings.get(settingsKey, "neverShowTerrainRulerHint")
|
|
||||||
) {
|
|
||||||
const lastHint = game.settings.get(settingsKey, "lastTerrainRulerHintTime");
|
const lastHint = game.settings.get(settingsKey, "lastTerrainRulerHintTime");
|
||||||
if (Date.now() - lastHint > 604800000) {
|
if (Date.now() - lastHint > 604800000) { // One week
|
||||||
// One week
|
|
||||||
let enabledTerrainModule;
|
let enabledTerrainModule;
|
||||||
if (game.modules.get("enhanced-terrain-layer")?.active) {
|
if (game.modules.get("enhanced-terrain-layer")?.active) {
|
||||||
enabledTerrainModule = game.modules.get("enhanced-terrain-layer").data.title;
|
enabledTerrainModule = game.modules.get("enhanced-terrain-layer").data.title;
|
||||||
@@ -87,25 +68,20 @@ export function checkDependencies() {
|
|||||||
if (enabledTerrainModule) {
|
if (enabledTerrainModule) {
|
||||||
new Dialog({
|
new Dialog({
|
||||||
title: game.i18n.localize("drag-ruler.dependencies.terrain-ruler.title"),
|
title: game.i18n.localize("drag-ruler.dependencies.terrain-ruler.title"),
|
||||||
content: `<h2>${game.i18n.localize(
|
content: `<h2>${game.i18n.localize("drag-ruler.dependencies.terrain-ruler.title")}</h2><p>${game.i18n.format("drag-ruler.dependencies.terrain-ruler.text", {moduleName: enabledTerrainModule})}</p>`,
|
||||||
"drag-ruler.dependencies.terrain-ruler.title",
|
|
||||||
)}</h2><p>${game.i18n.format("drag-ruler.dependencies.terrain-ruler.text", {
|
|
||||||
moduleName: enabledTerrainModule,
|
|
||||||
})}</p>`,
|
|
||||||
buttons: {
|
buttons: {
|
||||||
ok: {
|
ok: {
|
||||||
icon: '<i class="fas fa-check"></i>',
|
icon: '<i class="fas fa-check"></i>',
|
||||||
label: game.i18n.localize("drag-ruler.dependencies.ok"),
|
label: game.i18n.localize("drag-ruler.dependencies.ok"),
|
||||||
callback: () =>
|
callback: () => game.settings.set(settingsKey, "lastTerrainRulerHintTime", Date.now()),
|
||||||
game.settings.set(settingsKey, "lastTerrainRulerHintTime", Date.now()),
|
|
||||||
},
|
},
|
||||||
neverShowAgain: {
|
neverShowAgain: {
|
||||||
icon: '<i class="fas fa-times"></i>',
|
icon: '<i class="fas fa-times"></i>',
|
||||||
label: game.i18n.localize("drag-ruler.dependencies.terrain-ruler.neverShowAgain"),
|
label: game.i18n.localize("drag-ruler.dependencies.terrain-ruler.neverShowAgain"),
|
||||||
callback: () => game.settings.set(settingsKey, "neverShowTerrainRulerHint", true),
|
callback: () => game.settings.set(settingsKey, "neverShowTerrainRulerHint", true),
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
close: () => game.settings.set(settingsKey, "lastTerrainRulerHintTime", Date.now()),
|
close: () => game.settings.set(settingsKey, "lastTerrainRulerHintTime", Date.now())
|
||||||
}).render(true);
|
}).render(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,8 +110,8 @@ export class ProcessOnceQueue {
|
|||||||
const newNode = {
|
const newNode = {
|
||||||
value: element,
|
value: element,
|
||||||
next: null,
|
next: null,
|
||||||
previous: null,
|
previous: null
|
||||||
};
|
}
|
||||||
|
|
||||||
if (!this.first) {
|
if (!this.first) {
|
||||||
this.first = newNode;
|
this.first = newNode;
|
||||||
@@ -3,17 +3,18 @@
|
|||||||
// https://gitlab.com/foundrynet/foundryvtt/-/issues/4705
|
// https://gitlab.com/foundrynet/foundryvtt/-/issues/4705
|
||||||
export function getPixelsFromGridPosition(xGrid, yGrid) {
|
export function getPixelsFromGridPosition(xGrid, yGrid) {
|
||||||
if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS) {
|
if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS) {
|
||||||
return canvas.grid.grid.getPixelsFromGridPosition(yGrid, xGrid);
|
return canvas.grid.grid.getPixelsFromGridPosition(yGrid, xGrid)
|
||||||
}
|
}
|
||||||
return canvas.grid.grid.getPixelsFromGridPosition(xGrid, yGrid);
|
return canvas.grid.grid.getPixelsFromGridPosition(xGrid, yGrid)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrapper to fix a FoundryVTT bug that causes the return values of canvas.grid.grid.getPixelsFromGridPosition to be ordered inconsistently
|
// Wrapper to fix a FoundryVTT bug that causes the return values of canvas.grid.grid.getPixelsFromGridPosition to be ordered inconsistently
|
||||||
// https://gitlab.com/foundrynet/foundryvtt/-/issues/4705
|
// https://gitlab.com/foundrynet/foundryvtt/-/issues/4705
|
||||||
export function getGridPositionFromPixels(xPixel, yPixel) {
|
export function getGridPositionFromPixels(xPixel, yPixel) {
|
||||||
const [x, y] = canvas.grid.grid.getGridPositionFromPixels(xPixel, yPixel);
|
const [x, y] = canvas.grid.grid.getGridPositionFromPixels(xPixel, yPixel)
|
||||||
if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS) return [y, x];
|
if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS)
|
||||||
return [x, y];
|
return [y, x]
|
||||||
|
return [x, y]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getGridPositionFromPixelsObj(o) {
|
export function getGridPositionFromPixelsObj(o) {
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
import {highlightMeasurementTerrainRuler, measureDistances} from "./compatibility.js";
|
||||||
|
import {getGridPositionFromPixels, getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
|
||||||
|
import {Line} from "./geometry.js";
|
||||||
|
import {disableSnap, moveWithoutAnimation} from "./keybindings.js";
|
||||||
|
import {trackRays} from "./movement_tracking.js"
|
||||||
|
import {findPath, isPathfindingEnabled} from "./pathfinding.js";
|
||||||
|
import {settingsKey} from "./settings.js";
|
||||||
|
import {recalculate} from "./socket.js";
|
||||||
|
import {applyTokenSizeOffset, enumeratedZip, getSnapPointForEntity, getSnapPointForToken, getSnapPointForTokenObj, getTokenShape, highlightTokenShape, sum} from "./util.js";
|
||||||
|
|
||||||
|
// This is a modified version of Ruler.moveToken from foundry 0.7.9
|
||||||
|
export async function moveEntities(draggedEntity, selectedEntities) {
|
||||||
|
let wasPaused = game.paused;
|
||||||
|
if (wasPaused && !game.user.isGM) {
|
||||||
|
ui.notifications.warn(game.i18n.localize("GAME.PausedWarning"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!this.visible || !this.destination) return false;
|
||||||
|
if (!draggedEntity) return;
|
||||||
|
|
||||||
|
// Wait until all scheduled measurements are done
|
||||||
|
await this.deferredMeasurementPromise;
|
||||||
|
|
||||||
|
// Get the movement rays and check collision along each Ray
|
||||||
|
// These rays are center-to-center for the purposes of collision checking
|
||||||
|
const rays = this.constructor.dragRulerGetRaysFromWaypoints(this.waypoints, this.destination);
|
||||||
|
if (!game.user.isGM && draggedEntity instanceof Token) {
|
||||||
|
const hasCollision = selectedEntities.some(token => {
|
||||||
|
const offset = calculateEntityOffset(token, draggedEntity);
|
||||||
|
const offsetRays = rays.filter(ray => !ray.isPrevious).map(ray => applyOffsetToRay(ray, offset))
|
||||||
|
if (window.WallHeight) {
|
||||||
|
window.WallHeight.addBoundsToRays(offsetRays, draggedEntity);
|
||||||
|
}
|
||||||
|
return offsetRays.some(r => canvas.walls.checkCollision(r));
|
||||||
|
})
|
||||||
|
if (hasCollision) {
|
||||||
|
ui.notifications.error(game.i18n.localize("ERROR.TokenCollide"));
|
||||||
|
this._endMeasurement();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the movement path.
|
||||||
|
// Transform each center-to-center ray into a top-left to top-left ray using the prior token offsets.
|
||||||
|
this._state = Ruler.STATES.MOVING;
|
||||||
|
await animateEntities.call(this, selectedEntities, draggedEntity, rays, wasPaused);
|
||||||
|
|
||||||
|
// Once all animations are complete we can clear the ruler
|
||||||
|
if (this.draggedEntity?.id === draggedEntity.id)
|
||||||
|
this._endMeasurement();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a modified version code extracted from Ruler.moveToken from foundry 0.7.9
|
||||||
|
async function animateEntities(entities, draggedEntity, draggedRays, wasPaused) {
|
||||||
|
const newRays = draggedRays.filter(r => !r.isPrevious);
|
||||||
|
const entityAnimationData = entities.map(entity => {
|
||||||
|
const entityOffset = calculateEntityOffset(entity, draggedEntity);
|
||||||
|
const offsetRays = newRays.map(ray => applyOffsetToRay(ray, entityOffset));
|
||||||
|
|
||||||
|
// Determine offset relative to the Token top-left.
|
||||||
|
// This is important so we can position the token relative to the ruler origin for non-1x1 tokens.
|
||||||
|
const firstWaypoint = this.waypoints.find(w => !w.isPrevious);
|
||||||
|
const origin = [firstWaypoint.x + entityOffset.x, firstWaypoint.y + entityOffset.y];
|
||||||
|
let dx, dy;
|
||||||
|
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||||
|
dx = entity.data.x - origin[0];
|
||||||
|
dy = entity.data.y - origin[1];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dx = entity.data.x - origin[0];
|
||||||
|
dy = entity.data.y - origin[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {entity, rays: offsetRays, dx, dy};
|
||||||
|
});
|
||||||
|
|
||||||
|
const isToken = draggedEntity instanceof Token;
|
||||||
|
const animate = isToken && !moveWithoutAnimation;
|
||||||
|
const startWaypoint = animate ? 0 : entityAnimationData[0].rays.length - 1;
|
||||||
|
|
||||||
|
// This is a flag of the "Monk's Active Tile Triggers" module that signals that the movement should be cancelled early
|
||||||
|
this.cancelMovement = false;
|
||||||
|
|
||||||
|
for (let i = startWaypoint;i < entityAnimationData[0].rays.length; i++) {
|
||||||
|
if (!wasPaused && game.paused) break;
|
||||||
|
const entityPaths = entityAnimationData.map(({entity, rays, dx, dy}) => {
|
||||||
|
const ray = rays[i];
|
||||||
|
const dest = [ray.B.x, ray.B.y];
|
||||||
|
const path = new Ray({x: entity.x, y: entity.y}, {x: dest[0] + dx, y: dest[1] + dy});
|
||||||
|
return {entity, path};
|
||||||
|
});
|
||||||
|
const updates = entityPaths.map(({entity, path}) => {
|
||||||
|
return {x: path.B.x, y: path.B.y, _id: entity.id};
|
||||||
|
});
|
||||||
|
await draggedEntity.scene.updateEmbeddedDocuments(draggedEntity.constructor.embeddedName, updates, {animate});
|
||||||
|
if (animate)
|
||||||
|
await Promise.all(entityPaths.map(({entity}) => CanvasAnimation.getAnimation(entity.movementAnimationName)?.promise));
|
||||||
|
|
||||||
|
// This is a flag of the "Monk's Active Tile Triggers" module that signals that the movement should be cancelled early
|
||||||
|
if (this.cancelMovement) {
|
||||||
|
entityAnimationData.forEach(ead => ead.rays = ead.rays.slice(0, i + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isToken)
|
||||||
|
trackRays(entities, entityAnimationData.map(({rays}) => rays)).then(() => recalculate(entities));
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateEntityOffset(entityA, entityB) {
|
||||||
|
return {x: entityA.data.x - entityB.data.x, y: entityA.data.y - entityB.data.y};
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyOffsetToRay(ray, offset) {
|
||||||
|
const newRay = new Ray({x: ray.A.x + offset.x, y: ray.A.y + offset.y}, {x: ray.B.x + offset.x, y: ray.B.y + offset.y});
|
||||||
|
newRay.isPrevious = ray.isPrevious;
|
||||||
|
return newRay;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a modified version of Ruler._onMouseMove from foundry 0.7.9
|
||||||
|
export function onMouseMove(event) {
|
||||||
|
if (this._state === Ruler.STATES.MOVING) return;
|
||||||
|
|
||||||
|
// Extract event data
|
||||||
|
const destination = {x: event.data.destination.x + this.rulerOffset.x, y: event.data.destination.y + this.rulerOffset.y}
|
||||||
|
|
||||||
|
// Hide any existing Token HUD
|
||||||
|
canvas.hud.token.clear();
|
||||||
|
delete event.data.hudState;
|
||||||
|
|
||||||
|
// Draw measurement updates
|
||||||
|
scheduleMeasurement.call(this, destination, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleMeasurement(destination, event) {
|
||||||
|
const measurementInterval = 50;
|
||||||
|
const mt = event._measureTime || 0;
|
||||||
|
const originalEvent = event.data.originalEvent;
|
||||||
|
if (Date.now() - mt > measurementInterval) {
|
||||||
|
this.measure(destination, {snap: !disableSnap});
|
||||||
|
event._measureTime = Date.now();
|
||||||
|
this._state = Ruler.STATES.MEASURING;
|
||||||
|
cancelScheduledMeasurement.call(this);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.deferredMeasurementData = {destination, event};
|
||||||
|
if (!this.deferredMeasurementTimeout) {
|
||||||
|
this.deferredMeasurementPromise = new Promise((resolve, reject) => this.deferredMeasurementResolve = resolve);
|
||||||
|
this.deferredMeasurementTimeout = window.setTimeout(() => scheduleMeasurement.call(this, this.deferredMeasurementData.destination, this.deferredMeasurementData.event), measurementInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelScheduledMeasurement() {
|
||||||
|
window.clearTimeout(this.deferredMeasurementTimeout);
|
||||||
|
this.deferredMeasurementTimeout = undefined;
|
||||||
|
this.deferredMeasurementResolve?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a modified version of Ruler.measure form foundry 0.7.9
|
||||||
|
export function measure(destination, options={}) {
|
||||||
|
|
||||||
|
const isToken = this.draggedEntity instanceof Token;
|
||||||
|
if (isToken && !this.draggedEntity.isVisible)
|
||||||
|
return []
|
||||||
|
|
||||||
|
options.snap = options.snap ?? !disableSnap;
|
||||||
|
|
||||||
|
if (options.snap) {
|
||||||
|
destination = getSnapPointForEntity(destination.x, destination.y, this.draggedEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dragRulerRemovePathfindingWaypoints();
|
||||||
|
|
||||||
|
if (isToken && isPathfindingEnabled.call(this)) {
|
||||||
|
const from = getGridPositionFromPixelsObj(this.waypoints[this.waypoints.length - 1]);
|
||||||
|
const to = getGridPositionFromPixelsObj(destination);
|
||||||
|
let path = findPath(from, to, this.draggedEntity, this.waypoints);
|
||||||
|
if (path)
|
||||||
|
path.shift();
|
||||||
|
if (path && path.length > 0) {
|
||||||
|
path = path.map(point => getSnapPointForTokenObj(getPixelsFromGridPositionObj(point), this.draggedEntity));
|
||||||
|
|
||||||
|
// If the token is snapped to the grid, the first point of the path is already handled by the ruler
|
||||||
|
if (path[0].x === this.waypoints[this.waypoints.length - 1].x && path[0].y === this.waypoints[this.waypoints.length - 1].y)
|
||||||
|
path = path.slice(1);
|
||||||
|
|
||||||
|
// If snapping is enabled, the last point of the path is already handled by the ruler
|
||||||
|
if (options.snap)
|
||||||
|
path = path.slice(0, path.length - 1);
|
||||||
|
|
||||||
|
for (const point of path) {
|
||||||
|
point.isPathfinding = true;
|
||||||
|
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
|
||||||
|
}
|
||||||
|
this.waypoints = this.waypoints.concat(path);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Don't show a path if the pathfinding yields no result to show the user that the destination is unreachable
|
||||||
|
destination = this.waypoints[this.waypoints.length - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(options.gridSpaces === undefined) {
|
||||||
|
options.gridSpaces = canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canvas.grid.diagonalRule === "EUCL") {
|
||||||
|
options.gridSpaces = false;
|
||||||
|
options.ignoreGrid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(options.ignoreGrid === undefined) {
|
||||||
|
options.ignoreGrid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.enableTerrainRuler = isToken && game.modules.get("terrain-ruler")?.active;
|
||||||
|
|
||||||
|
const waypoints = this.waypoints.concat([destination]);
|
||||||
|
// Move the waypoints to the center of the grid if a size is used that measures from edge to edge
|
||||||
|
const centeredWaypoints = isToken ? applyTokenSizeOffset(waypoints, this.draggedEntity) : duplicate(waypoints);
|
||||||
|
// Foundries native ruler requires the waypoints to sit in the dead center of the square to work properly
|
||||||
|
if (!options.enableTerrainRuler && !options.ignoreGrid)
|
||||||
|
centeredWaypoints.forEach(w => [w.x, w.y] = canvas.grid.getCenter(w.x, w.y));
|
||||||
|
|
||||||
|
const r = this.ruler;
|
||||||
|
this.destination = destination;
|
||||||
|
|
||||||
|
// Iterate over waypoints and construct segment rays
|
||||||
|
const segments = [];
|
||||||
|
const centeredSegments = []
|
||||||
|
for (let [i, dest] of waypoints.slice(1).entries()) {
|
||||||
|
const centeredDest = centeredWaypoints[i + 1]
|
||||||
|
const origin = waypoints[i];
|
||||||
|
const centeredOrigin = centeredWaypoints[i]
|
||||||
|
const label = this.labels.children[i];
|
||||||
|
const ray = new Ray(origin, dest);
|
||||||
|
const centeredRay = new Ray(centeredOrigin, centeredDest)
|
||||||
|
ray.isPrevious = Boolean(origin.isPrevious);
|
||||||
|
centeredRay.isPrevious = ray.isPrevious;
|
||||||
|
ray.dragRulerVisitedSpaces = origin.dragRulerVisitedSpaces;
|
||||||
|
centeredRay.dragRulerVisitedSpaces = ray.dragRulerVisitedSpaces;
|
||||||
|
ray.dragRulerFinalState = origin.dragRulerFinalState;
|
||||||
|
centeredRay.dragRulerFinalState = ray.dragRulerFinalState;
|
||||||
|
if (ray.distance < 10) {
|
||||||
|
if (label) label.visible = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
segments.push({ ray, label });
|
||||||
|
centeredSegments.push({ray: centeredRay, label})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const shape = isToken ? getTokenShape(this.draggedEntity) : null;
|
||||||
|
|
||||||
|
// Compute measured distance
|
||||||
|
const distances = measureDistances(centeredSegments, this.draggedEntity, shape, options);
|
||||||
|
|
||||||
|
let totalDistance = 0;
|
||||||
|
for (let [i, d] of distances.entries()) {
|
||||||
|
let s = centeredSegments[i];
|
||||||
|
s.startDistance = totalDistance
|
||||||
|
totalDistance += d;
|
||||||
|
s.last = i === (centeredSegments.length - 1);
|
||||||
|
s.distance = d;
|
||||||
|
s.text = this._getSegmentLabel(d, totalDistance, s.last);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the grid highlight layer
|
||||||
|
const hlt = canvas.grid.highlightLayers[this.name] || canvas.grid.addHighlightLayer(this.name);
|
||||||
|
hlt.clear();
|
||||||
|
|
||||||
|
// Draw measured path
|
||||||
|
r.clear();
|
||||||
|
let rulerColor
|
||||||
|
if (!options.gridSpaces || canvas.grid.type === CONST.GRID_TYPES.GRIDLESS)
|
||||||
|
rulerColor = this.dragRulerGetColorForDistance(totalDistance);
|
||||||
|
else
|
||||||
|
rulerColor = this.color
|
||||||
|
for (const [i, s, cs] of enumeratedZip([...segments].reverse(), [...centeredSegments].reverse())) {
|
||||||
|
const { label, text, last } = cs;
|
||||||
|
|
||||||
|
// Draw line segment
|
||||||
|
const opacityMultiplier = s.ray.isPrevious ? 0.33 : 1;
|
||||||
|
r.lineStyle(6, 0x000000, 0.5 * opacityMultiplier).moveTo(s.ray.A.x, s.ray.A.y).lineTo(s.ray.B.x, s.ray.B.y)
|
||||||
|
.lineStyle(4, rulerColor, 0.25 * opacityMultiplier).moveTo(s.ray.A.x, s.ray.A.y).lineTo(s.ray.B.x, s.ray.B.y);
|
||||||
|
|
||||||
|
// Draw the distance label just after the endpoint of the segment
|
||||||
|
if (label) {
|
||||||
|
label.text = text;
|
||||||
|
label.alpha = last ? 1.0 : 0.5;
|
||||||
|
label.visible = true;
|
||||||
|
let labelPosition = {x: s.ray.x0, y: s.ray.y0};
|
||||||
|
labelPosition.x -= label.width / 2;
|
||||||
|
labelPosition.y -= label.height / 2;
|
||||||
|
const rayLine = Line.fromPoints(s.ray.A, s.ray.B);
|
||||||
|
const rayLabelXHitY = rayLine.calcY(labelPosition.x);
|
||||||
|
let innerDistance;
|
||||||
|
// If ray hits top or bottom side of label
|
||||||
|
if (rayLine.isVertical || rayLabelXHitY < labelPosition.y || rayLabelXHitY > labelPosition.y + label.height)
|
||||||
|
innerDistance = Math.abs((label.height / 2) / Math.sin(s.ray.angle));
|
||||||
|
// If ray hits left or right side of label
|
||||||
|
else
|
||||||
|
innerDistance = Math.abs((label.width / 2) / Math.cos(s.ray.angle));
|
||||||
|
labelPosition = s.ray.project((s.ray.distance + 50 + innerDistance) / s.ray.distance);
|
||||||
|
labelPosition.x -= label.width / 2;
|
||||||
|
labelPosition.y -= label.height / 2;
|
||||||
|
label.position.set(labelPosition.x, labelPosition.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight grid positions
|
||||||
|
if (isToken && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS && options.gridSpaces) {
|
||||||
|
if (options.enableTerrainRuler) {
|
||||||
|
highlightMeasurementTerrainRuler.call(this, cs.ray, cs.startDistance, shape, opacityMultiplier)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const previousSegments = centeredSegments.slice(0, segments.length - 1 - i);
|
||||||
|
highlightMeasurementNative.call(this, cs.ray, previousSegments, shape, opacityMultiplier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw endpoints
|
||||||
|
for (let p of waypoints) {
|
||||||
|
r.lineStyle(2, 0x000000, 0.5).beginFill(rulerColor, 0.25).drawCircle(p.x, p.y, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the measured segments
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function highlightMeasurementNative(ray, previousSegments, tokenShape=[{x: 0, y: 0}], alpha=1) {
|
||||||
|
const spacer = canvas.scene.data.gridType === CONST.GRID_TYPES.SQUARE ? 1.41 : 1;
|
||||||
|
const nMax = Math.max(Math.floor(ray.distance / (spacer * Math.min(canvas.grid.w, canvas.grid.h))), 1);
|
||||||
|
const tMax = Array.fromRange(nMax+1).map(t => t / nMax);
|
||||||
|
|
||||||
|
// Track prior position
|
||||||
|
let prior = null;
|
||||||
|
|
||||||
|
// Iterate over ray portions
|
||||||
|
for ( let [i, t] of tMax.reverse().entries() ) {
|
||||||
|
let {x, y} = ray.project(t);
|
||||||
|
|
||||||
|
// Get grid position
|
||||||
|
let [x0, y0] = (i === 0) ? [null, null] : prior;
|
||||||
|
let [x1, y1] = canvas.grid.grid.getGridPositionFromPixels(x, y);
|
||||||
|
if ( x0 === x1 && y0 === y1 ) continue;
|
||||||
|
|
||||||
|
// Highlight the grid position
|
||||||
|
let [xgtl, ygtl] = canvas.grid.grid.getPixelsFromGridPosition(x1, y1);
|
||||||
|
let [xg, yg] = canvas.grid.grid.getCenter(xgtl, ygtl);
|
||||||
|
const pathUntilSpace = previousSegments.concat([{ray: new Ray(ray.A, {x: xg, y: yg})}]);
|
||||||
|
const distance = sum(canvas.grid.measureDistances(pathUntilSpace, {gridSpaces: true}));
|
||||||
|
const color = this.dragRulerGetColorForDistance(distance);
|
||||||
|
const snapPoint = getSnapPointForToken(...canvas.grid.getTopLeft(x, y), this.draggedEntity);
|
||||||
|
const [snapX, snapY] = getGridPositionFromPixels(snapPoint.x + 1, snapPoint.y + 1);
|
||||||
|
|
||||||
|
prior = [x1, y1];
|
||||||
|
|
||||||
|
// If the positions are not neighbors, also highlight their halfway point
|
||||||
|
if (i > 0 && !canvas.grid.isNeighbor(x0, y0, x1, y1)) {
|
||||||
|
let th = tMax[i - 1] - (0.5 / nMax);
|
||||||
|
let {x, y} = ray.project(th);
|
||||||
|
let [x1h, y1h] = canvas.grid.grid.getGridPositionFromPixels(x, y);
|
||||||
|
let [xghtl, yghtl] = canvas.grid.grid.getPixelsFromGridPosition(x1h, y1h);
|
||||||
|
let [xgh, ygh] = canvas.grid.grid.getCenter(xghtl, yghtl);
|
||||||
|
const pathUntilSpace = previousSegments.concat([{ray: new Ray(ray.A, {x: xgh, y: ygh})}]);
|
||||||
|
const distance = sum(canvas.grid.measureDistances(pathUntilSpace, {gridSpaces: true}));
|
||||||
|
const color = this.dragRulerGetColorForDistance(distance);
|
||||||
|
const snapPoint = getSnapPointForToken(...canvas.grid.getTopLeft(x, y), this.draggedEntity);
|
||||||
|
const [snapX, snapY] = getGridPositionFromPixels(snapPoint.x + 1, snapPoint.y + 1);
|
||||||
|
highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color, alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,73 +18,76 @@ export function findVertexSnapPoint(x, y, altOrientationFlag) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function findSnapPointRows(x, y, h, w, alt) {
|
function findSnapPointRows(x, y, h, w, alt) {
|
||||||
let xOffset = 0.0;
|
let xOffset = 0.0
|
||||||
if (canvas.grid.grid.even) {
|
if (canvas.grid.grid.even) {
|
||||||
xOffset = -0.5;
|
xOffset = -0.5
|
||||||
}
|
}
|
||||||
|
|
||||||
let yOffset1 = 0.75;
|
let yOffset1 = 0.75
|
||||||
let yOffset2 = 0.0;
|
let yOffset2 = 0.00
|
||||||
if (alt) {
|
if (alt) {
|
||||||
yOffset1 = 0.25;
|
yOffset1 = 0.25
|
||||||
yOffset2 = 1.0;
|
yOffset2 = 1.00
|
||||||
}
|
}
|
||||||
|
|
||||||
let row1 = calculateSnapPointsRows(x, y, h, w, 0.5 + xOffset, yOffset1);
|
let row1 = calculateSnapPointsRows(x, y, h, w, 0.5 + xOffset, yOffset1);
|
||||||
let row2 = calculateSnapPointsRows(x, y, h, w, 1.0 + xOffset, yOffset2);
|
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 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);
|
let dist2 = Math.pow((row2.x - x), 2) + Math.pow((row2.y - y), 2)
|
||||||
|
|
||||||
if (dist1 < dist2) {
|
if (dist1 < dist2) {
|
||||||
return row1;
|
return row1
|
||||||
} else {
|
}
|
||||||
return row2;
|
else {
|
||||||
|
return row2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateSnapPointsRows(x, y, h, w, xOff, yOff) {
|
function calculateSnapPointsRows(x, y, h, w, xOff, yOff) {
|
||||||
let c = Math.floor((x + (0.5 - xOff) * w) / w + 1);
|
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 r = Math.floor(((y + ((0.75 - yOff) * h)) / (1.5 * h)) + 1)
|
||||||
|
|
||||||
let snapX = c * w - (1 - xOff) * w;
|
let snapX = (c * w) - ((1 - xOff) * w)
|
||||||
let snapY = r * h * 1.5 - (1.5 - yOff) * h;
|
let snapY = (r * h * 1.5) - ((1.5 - yOff) * h)
|
||||||
|
|
||||||
return {x: snapX, y: snapY};
|
return {x: snapX, y: snapY}
|
||||||
}
|
}
|
||||||
|
|
||||||
function findSnapPointCols(x, y, h, w, alt) {
|
function findSnapPointCols(x, y, h, w, alt) {
|
||||||
let yOffset = 0.0;
|
let yOffset = 0.0
|
||||||
if (canvas.grid.grid.even) {
|
if (canvas.grid.grid.even) {
|
||||||
yOffset = -0.5;
|
yOffset = -0.5
|
||||||
}
|
}
|
||||||
|
|
||||||
let xOffset1 = 0.25;
|
let xOffset1 = 0.25
|
||||||
let xOffset2 = 1.0;
|
let xOffset2 = 1.00
|
||||||
if (alt) {
|
if (alt) {
|
||||||
xOffset1 = 0.75;
|
xOffset1 = 0.75
|
||||||
xOffset2 = 0.0;
|
xOffset2 = 0.00
|
||||||
}
|
}
|
||||||
|
|
||||||
let row1 = calculateSnapPointsCols(x, y, h, w, xOffset1, 0.5 + yOffset);
|
let row1 = calculateSnapPointsCols(x, y, h, w, xOffset1, 0.5 + yOffset);
|
||||||
let row2 = calculateSnapPointsCols(x, y, h, w, xOffset2, 1.0 + 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 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);
|
let dist2 = Math.pow((row2.x - x), 2) + Math.pow((row2.y - y), 2)
|
||||||
|
|
||||||
if (dist1 < dist2) {
|
if (dist1 < dist2) {
|
||||||
return row1;
|
return row1
|
||||||
} else {
|
|
||||||
return row2;
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
return row2
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateSnapPointsCols(x, y, h, w, xOff, yOff) {
|
function calculateSnapPointsCols(x, y, h, w, xOff, yOff) {
|
||||||
let c = Math.floor((x + (0.75 - xOff) * w) / (1.5 * w) + 1);
|
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 r = Math.floor(((y + ((0.5 - yOff) * h)) / h) + 1)
|
||||||
|
|
||||||
let snapX = c * w * 1.5 - (1.5 - xOff) * w;
|
let snapX = (c * w * 1.5) - ((1.5 - xOff) * w)
|
||||||
let snapY = r * h - (1 - yOff) * h;
|
let snapY = (r * h) - ((1 - yOff) * h)
|
||||||
|
|
||||||
return {x: snapX, y: snapY};
|
return {x: snapX, y: snapY}
|
||||||
}
|
}
|
||||||
@@ -1,35 +1,26 @@
|
|||||||
import {settingsKey} from "./settings.js";
|
import {settingsKey} from "./settings.js";
|
||||||
import {getMeasurePosition, setSnapParameterOnOptions} from "./util.js";
|
import {getMeasurePosition, setSnapParameterOnOptions} from "./util.js";
|
||||||
|
|
||||||
// Indicates whether the user is currently pressing the button to disable snapping
|
|
||||||
export let disableSnap = false;
|
export let disableSnap = false;
|
||||||
|
|
||||||
// Indicates whether the user is currently pressing the button to disable the movement animation
|
|
||||||
export let moveWithoutAnimation = false;
|
export let moveWithoutAnimation = false;
|
||||||
|
|
||||||
// Indicates whether the user is pressing the pathfinding toggle button
|
|
||||||
export let togglePathfinding = false;
|
export let togglePathfinding = false;
|
||||||
|
|
||||||
export function registerKeybindings() {
|
export function registerKeybindings() {
|
||||||
game.keybindings.register(settingsKey, "cancelDrag", {
|
game.keybindings.register(settingsKey, "cancelDrag", {
|
||||||
name: "drag-ruler.keybindings.cancelDrag",
|
name: "drag-ruler.keybindings.cancelDrag",
|
||||||
onDown: cancelDrag,
|
onDown: cancelDrag,
|
||||||
uneditable: [
|
uneditable: [{
|
||||||
{
|
key: "Escape",
|
||||||
key: "Escape",
|
}],
|
||||||
},
|
|
||||||
],
|
|
||||||
precedence: -1,
|
precedence: -1,
|
||||||
});
|
});
|
||||||
|
|
||||||
game.keybindings.register(settingsKey, "createWaypoint", {
|
game.keybindings.register(settingsKey, "createWaypoint", {
|
||||||
name: "drag-ruler.keybindings.createWaypoint",
|
name: "drag-ruler.keybindings.createWaypoint",
|
||||||
onDown: handleCreateWaypoint,
|
onDown: handleCreateWaypoint,
|
||||||
editable: [
|
editable: [{
|
||||||
{
|
key: "Space"
|
||||||
key: "Space",
|
}],
|
||||||
},
|
|
||||||
],
|
|
||||||
precedence: -1,
|
precedence: -1,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -44,11 +35,9 @@ export function registerKeybindings() {
|
|||||||
hint: "drag-ruler.keybindings.disableSnap.hint",
|
hint: "drag-ruler.keybindings.disableSnap.hint",
|
||||||
onDown: handleDisableSnap,
|
onDown: handleDisableSnap,
|
||||||
onUp: handleDisableSnap,
|
onUp: handleDisableSnap,
|
||||||
editable: [
|
editable: [{
|
||||||
{
|
key: "ShiftLeft",
|
||||||
key: "ShiftLeft",
|
}],
|
||||||
},
|
|
||||||
],
|
|
||||||
precedence: -1,
|
precedence: -1,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -57,29 +46,26 @@ export function registerKeybindings() {
|
|||||||
hint: "drag-ruler.keybindings.moveWithoutAnimation.hint",
|
hint: "drag-ruler.keybindings.moveWithoutAnimation.hint",
|
||||||
onDown: handleMoveWithoutAnimation,
|
onDown: handleMoveWithoutAnimation,
|
||||||
onUp: handleMoveWithoutAnimation,
|
onUp: handleMoveWithoutAnimation,
|
||||||
editable: [
|
editable: [{
|
||||||
{
|
key: "AltLeft",
|
||||||
key: "AltLeft",
|
}],
|
||||||
},
|
|
||||||
],
|
|
||||||
precedence: -1,
|
precedence: -1,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (game.modules.get("routinglib")?.active) {
|
game.keybindings.register(settingsKey, "togglePathfinding", {
|
||||||
game.keybindings.register(settingsKey, "togglePathfinding", {
|
name: "drag-ruler.keybindings.togglePathfinding.name",
|
||||||
name: "drag-ruler.keybindings.togglePathfinding.name",
|
hint: "drag-ruler.keybindings.togglePathfinding.hint",
|
||||||
hint: "drag-ruler.keybindings.togglePathfinding.hint",
|
onDown: handleTogglePathfinding,
|
||||||
onDown: handleTogglePathfinding,
|
onUp: handleTogglePathfinding,
|
||||||
onUp: handleTogglePathfinding,
|
precedence: -1,
|
||||||
precedence: -1,
|
restricted: !game.settings.get(settingsKey, "allowPathfinding"),
|
||||||
restricted: !game.settings.get(settingsKey, "allowPathfinding"),
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDeleteWaypoint() {
|
function handleDeleteWaypoint() {
|
||||||
const ruler = canvas.controls.ruler;
|
const ruler = canvas.controls.ruler;
|
||||||
if (!ruler?.draggedEntity) return false;
|
if (!ruler?.draggedEntity)
|
||||||
|
return false;
|
||||||
ruler.dragRulerDeleteWaypoint();
|
ruler.dragRulerDeleteWaypoint();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -88,14 +74,16 @@ function handleCreateWaypoint() {
|
|||||||
const ruler = canvas.controls.ruler;
|
const ruler = canvas.controls.ruler;
|
||||||
// .draggedEntity is used here because .isDragRuler only returns true once the ruler started measuring
|
// .draggedEntity is used here because .isDragRuler only returns true once the ruler started measuring
|
||||||
// Ruler can end up being undefined here if no canvas is active
|
// Ruler can end up being undefined here if no canvas is active
|
||||||
if (!ruler?.draggedEntity) return false;
|
if (!ruler?.draggedEntity)
|
||||||
|
return false;
|
||||||
|
|
||||||
let options = {};
|
let options = {};
|
||||||
setSnapParameterOnOptions(ruler, options);
|
setSnapParameterOnOptions(ruler, options);
|
||||||
|
|
||||||
if (ruler._state === Ruler.STATES.INACTIVE) {
|
if (ruler._state === Ruler.STATES.INACTIVE) {
|
||||||
ruler.dragRulerStart(options);
|
ruler.dragRulerStart(options);
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
ruler.dragRulerAddWaypoint(getMeasurePosition(), options);
|
ruler.dragRulerAddWaypoint(getMeasurePosition(), options);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -103,7 +91,8 @@ function handleCreateWaypoint() {
|
|||||||
|
|
||||||
function cancelDrag() {
|
function cancelDrag() {
|
||||||
const ruler = canvas.controls.ruler;
|
const ruler = canvas.controls.ruler;
|
||||||
if (!ruler?.draggedEntity) return false;
|
if (!ruler?.draggedEntity)
|
||||||
|
return false;
|
||||||
ruler.dragRulerAbortDrag();
|
ruler.dragRulerAbortDrag();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -112,8 +101,10 @@ function handleDisableSnap(event) {
|
|||||||
disableSnap = !event.up;
|
disableSnap = !event.up;
|
||||||
|
|
||||||
const ruler = canvas.controls.ruler;
|
const ruler = canvas.controls.ruler;
|
||||||
if (!ruler?.isDragRuler) return false;
|
if (!ruler?.isDragRuler)
|
||||||
if (ruler._state !== Ruler.STATES.MEASURING) return false;
|
return false;
|
||||||
|
if (ruler._state !== Ruler.STATES.MEASURING)
|
||||||
|
return false;
|
||||||
|
|
||||||
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
|
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
|
||||||
ruler.dragRulerSendState();
|
ruler.dragRulerSendState();
|
||||||
@@ -124,8 +115,10 @@ function handleMoveWithoutAnimation(event) {
|
|||||||
moveWithoutAnimation = !event.up;
|
moveWithoutAnimation = !event.up;
|
||||||
|
|
||||||
const ruler = canvas.controls.ruler;
|
const ruler = canvas.controls.ruler;
|
||||||
if (!ruler?.isDragRuler) return false;
|
if (!ruler?.isDragRuler)
|
||||||
if (ruler._state !== Ruler.STATES.MEASURING) return false;
|
return false;
|
||||||
|
if (ruler._state !== Ruler.STATES.MEASURING)
|
||||||
|
return false;
|
||||||
|
|
||||||
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
|
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
|
||||||
ruler.dragRulerSendState();
|
ruler.dragRulerSendState();
|
||||||
@@ -136,8 +129,10 @@ function handleTogglePathfinding(event) {
|
|||||||
togglePathfinding = !event.up;
|
togglePathfinding = !event.up;
|
||||||
|
|
||||||
const ruler = canvas.controls.ruler;
|
const ruler = canvas.controls.ruler;
|
||||||
if (!ruler?.isDragRuler) return false;
|
if (!ruler?.isDragRuler)
|
||||||
if (ruler._state !== Ruler.STATES.MEASURING) return false;
|
return false;
|
||||||
|
if (ruler._state !== Ruler.STATES.MEASURING)
|
||||||
|
return false;
|
||||||
|
|
||||||
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
|
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
|
||||||
ruler.dragRulerSendState();
|
ruler.dragRulerSendState();
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright © 2021 fvtt-lib-wrapper Rui Pinheiro
|
||||||
|
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// A shim for the libWrapper library
|
||||||
|
export let libWrapper = undefined;
|
||||||
|
|
||||||
|
export const VERSIONS = [1,11,0];
|
||||||
|
export const TGT_SPLIT_RE = new RegExp("([^.[]+|\\[('([^'\\\\]|\\\\.)+?'|\"([^\"\\\\]|\\\\.)+?\")\\])", 'g');
|
||||||
|
export const TGT_CLEANUP_RE = new RegExp("(^\\['|'\\]$|^\\[\"|\"\\]$)", 'g');
|
||||||
|
|
||||||
|
// Main shim code
|
||||||
|
Hooks.once('init', () => {
|
||||||
|
// Check if the real module is already loaded - if so, use it
|
||||||
|
if(globalThis.libWrapper && !(globalThis.libWrapper.is_fallback ?? true)) {
|
||||||
|
libWrapper = globalThis.libWrapper;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback implementation
|
||||||
|
libWrapper = class {
|
||||||
|
static get is_fallback() { return true };
|
||||||
|
|
||||||
|
static get WRAPPER() { return 'WRAPPER' };
|
||||||
|
static get MIXED() { return 'MIXED' };
|
||||||
|
static get OVERRIDE() { return 'OVERRIDE' };
|
||||||
|
|
||||||
|
static register(package_id, target, fn, type="MIXED", {chain=undefined}={}) {
|
||||||
|
const is_setter = target.endsWith('#set');
|
||||||
|
target = !is_setter ? target : target.slice(0, -4);
|
||||||
|
const split = target.match(TGT_SPLIT_RE).map((x)=>x.replace(/\\(.)/g, '$1').replace(TGT_CLEANUP_RE,''));
|
||||||
|
const root_nm = split.splice(0,1)[0];
|
||||||
|
|
||||||
|
let obj, fn_name;
|
||||||
|
if(split.length == 0) {
|
||||||
|
obj = globalThis;
|
||||||
|
fn_name = root_nm;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const _eval = eval;
|
||||||
|
fn_name = split.pop();
|
||||||
|
obj = split.reduce((x,y)=>x[y], globalThis[root_nm] ?? _eval(root_nm));
|
||||||
|
}
|
||||||
|
|
||||||
|
let iObj = obj;
|
||||||
|
let descriptor = null;
|
||||||
|
while(iObj) {
|
||||||
|
descriptor = Object.getOwnPropertyDescriptor(iObj, fn_name);
|
||||||
|
if(descriptor) break;
|
||||||
|
iObj = Object.getPrototypeOf(iObj);
|
||||||
|
}
|
||||||
|
if(!descriptor || descriptor?.configurable === false) throw `libWrapper Shim: '${target}' does not exist, could not be found, or has a non-configurable descriptor.`;
|
||||||
|
|
||||||
|
let original = null;
|
||||||
|
const wrapper = (chain ?? (type.toUpperCase?.() != 'OVERRIDE' && type != 3)) ? function() { return fn.call(this, original.bind(this), ...arguments); } : function() { return fn.apply(this, arguments); };
|
||||||
|
|
||||||
|
if(!is_setter) {
|
||||||
|
if(descriptor.value) {
|
||||||
|
original = descriptor.value;
|
||||||
|
descriptor.value = wrapper;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
original = descriptor.get;
|
||||||
|
descriptor.get = wrapper;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(!descriptor.set) throw `libWrapper Shim: '${target}' does not have a setter`;
|
||||||
|
original = descriptor.set;
|
||||||
|
descriptor.set = wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptor.configurable = true;
|
||||||
|
Object.defineProperty(obj, fn_name, descriptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
+103
-110
@@ -1,40 +1,57 @@
|
|||||||
"use strict";
|
"use strict"
|
||||||
|
|
||||||
import {
|
import {getColorForDistanceAndToken, getMovedDistanceFromToken, getRangesFromSpeedProvider, initApi, registerModule, registerSystem} from "./api.js";
|
||||||
getColorForDistanceAndToken,
|
|
||||||
getMovedDistanceFromToken,
|
|
||||||
getRangesFromSpeedProvider,
|
|
||||||
initApi,
|
|
||||||
registerModule,
|
|
||||||
registerSystem,
|
|
||||||
} from "./api.js";
|
|
||||||
import {checkDependencies, getHexSizeSupportTokenGridCenter} from "./compatibility.js";
|
import {checkDependencies, getHexSizeSupportTokenGridCenter} from "./compatibility.js";
|
||||||
import {moveEntities, onMouseMove} from "./foundry_imports.js";
|
import {moveEntities, onMouseMove} from "./foundry_imports.js"
|
||||||
import {disableSnap, registerKeybindings} from "./keybindings.js";
|
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, startBackgroundCaching, terrainRulerWrapper} 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";
|
||||||
import {SpeedProvider} from "./speed_provider.js";
|
import {SpeedProvider} from "./speed_provider.js"
|
||||||
import {setSnapParameterOnOptions} from "./util.js";
|
import {setSnapParameterOnOptions} from "./util.js";
|
||||||
|
|
||||||
|
import initGridlessPathfinding, * as GridlessPathfinding from "../wasm/gridless_pathfinding.js"
|
||||||
|
|
||||||
CONFIG.debug.dragRuler = false;
|
CONFIG.debug.dragRuler = false;
|
||||||
export let debugGraphics = undefined;
|
export let debugGraphics = undefined;
|
||||||
|
|
||||||
|
initGridlessPathfinding().then(() => {
|
||||||
|
Hooks.on("canvasInit", wipePathfindingCache);
|
||||||
|
Hooks.on("canvasReady", () => {
|
||||||
|
wipePathfindingCache();
|
||||||
|
initializePathfinding();
|
||||||
|
});
|
||||||
|
Hooks.on("createWall", wipePathfindingCache);
|
||||||
|
Hooks.on("updateWall", 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", () => {
|
||||||
registerSettings();
|
registerSettings()
|
||||||
registerKeybindings();
|
registerKeybindings()
|
||||||
initApi();
|
initApi()
|
||||||
hookDragHandlers(Token);
|
hookDragHandlers(Token);
|
||||||
hookDragHandlers(MeasuredTemplate);
|
hookDragHandlers(MeasuredTemplate);
|
||||||
libWrapper.register(
|
libWrapper.register("drag-ruler", "TokenLayer.prototype.undoHistory", tokenLayerUndoHistory, "WRAPPER");
|
||||||
"drag-ruler",
|
|
||||||
"TokenLayer.prototype.undoHistory",
|
|
||||||
tokenLayerUndoHistory,
|
|
||||||
"WRAPPER",
|
|
||||||
);
|
|
||||||
|
|
||||||
extendRuler();
|
extendRuler();
|
||||||
|
|
||||||
@@ -45,15 +62,19 @@ Hooks.once("init", () => {
|
|||||||
registerSystem,
|
registerSystem,
|
||||||
recalculate,
|
recalculate,
|
||||||
resetMovementHistory,
|
resetMovementHistory,
|
||||||
};
|
private: {
|
||||||
});
|
terrainRulerWrapper
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
Hooks.once("ready", () => {
|
Hooks.once("ready", () => {
|
||||||
performMigrations();
|
performMigrations()
|
||||||
checkDependencies();
|
checkDependencies();
|
||||||
Hooks.callAll("dragRuler.ready", SpeedProvider);
|
Hooks.callAll("dragRuler.ready", SpeedProvider)
|
||||||
if (CONFIG.debug.dragRuler) debugGraphics = canvas.controls.addChild(new PIXI.Container());
|
if (CONFIG.debug.dragRuler)
|
||||||
});
|
debugGraphics = canvas.controls.addChild(new PIXI.Container());
|
||||||
|
})
|
||||||
|
|
||||||
Hooks.on("canvasReady", () => {
|
Hooks.on("canvasReady", () => {
|
||||||
canvas.controls.rulers.children.forEach(ruler => {
|
canvas.controls.rulers.children.forEach(ruler => {
|
||||||
@@ -61,61 +82,37 @@ Hooks.on("canvasReady", () => {
|
|||||||
Object.defineProperty(ruler, "isDragRuler", {
|
Object.defineProperty(ruler, "isDragRuler", {
|
||||||
get: function isDragRuler() {
|
get: function isDragRuler() {
|
||||||
return Boolean(this.draggedEntity) && this._state !== Ruler.STATES.INACTIVE;
|
return Boolean(this.draggedEntity) && this._state !== Ruler.STATES.INACTIVE;
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
Hooks.on("getCombatTrackerEntryContext", function (html, menu) {
|
Hooks.on("getCombatTrackerEntryContext", function (html, menu) {
|
||||||
const entry = {
|
const entry = {
|
||||||
name: "drag-ruler.resetMovementHistory",
|
name: "drag-ruler.resetMovementHistory",
|
||||||
icon: '<i class="fas fa-undo-alt"></i>',
|
icon: '<i class="fas fa-undo-alt"></i>',
|
||||||
callback: li => resetMovementHistory(ui.combat.viewed, li.data("combatant-id")),
|
callback: li => resetMovementHistory(ui.combat.viewed, li.data('combatant-id')),
|
||||||
};
|
};
|
||||||
menu.splice(1, 0, entry);
|
menu.splice(1, 0, entry);
|
||||||
});
|
});
|
||||||
|
|
||||||
function forwardIfUnahndled(newFn) {
|
function forwardIfUnahndled(newFn) {
|
||||||
return function (oldFn, ...args) {
|
return function(oldFn, ...args) {
|
||||||
const eventHandled = newFn(...args);
|
const eventHandled = newFn(...args);
|
||||||
if (!eventHandled) oldFn(...args);
|
if (!eventHandled)
|
||||||
|
oldFn(...args);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function hookDragHandlers(entityType) {
|
function hookDragHandlers(entityType) {
|
||||||
const entityName = entityType.name;
|
const entityName = entityType.name
|
||||||
libWrapper.register(
|
libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftStart`, onEntityLeftDragStart, "WRAPPER");
|
||||||
"drag-ruler",
|
|
||||||
`${entityName}.prototype._onDragLeftStart`,
|
|
||||||
onEntityLeftDragStart,
|
|
||||||
"WRAPPER",
|
|
||||||
);
|
|
||||||
if (entityType === Token)
|
if (entityType === Token)
|
||||||
libWrapper.register(
|
libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftMove`, onEntityLeftDragMoveSnap, "WRAPPER");
|
||||||
"drag-ruler",
|
|
||||||
`${entityName}.prototype._onDragLeftMove`,
|
|
||||||
onEntityLeftDragMoveSnap,
|
|
||||||
"WRAPPER",
|
|
||||||
);
|
|
||||||
else
|
else
|
||||||
libWrapper.register(
|
libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftMove`, onEntityLeftDragMove, "WRAPPER");
|
||||||
"drag-ruler",
|
libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftDrop`, forwardIfUnahndled(onEntityDragLeftDrop), "MIXED");
|
||||||
`${entityName}.prototype._onDragLeftMove`,
|
libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftCancel`, forwardIfUnahndled(onEntityDragLeftCancel), "MIXED");
|
||||||
onEntityLeftDragMove,
|
|
||||||
"WRAPPER",
|
|
||||||
);
|
|
||||||
libWrapper.register(
|
|
||||||
"drag-ruler",
|
|
||||||
`${entityName}.prototype._onDragLeftDrop`,
|
|
||||||
forwardIfUnahndled(onEntityDragLeftDrop),
|
|
||||||
"MIXED",
|
|
||||||
);
|
|
||||||
libWrapper.register(
|
|
||||||
"drag-ruler",
|
|
||||||
`${entityName}.prototype._onDragLeftCancel`,
|
|
||||||
forwardIfUnahndled(onEntityDragLeftCancel),
|
|
||||||
"MIXED",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tokenLayerUndoHistory(wrapped) {
|
async function tokenLayerUndoHistory(wrapped) {
|
||||||
@@ -133,21 +130,14 @@ async function tokenLayerUndoHistory(wrapped) {
|
|||||||
function onEntityLeftDragStart(wrapped, event) {
|
function onEntityLeftDragStart(wrapped, event) {
|
||||||
wrapped(event);
|
wrapped(event);
|
||||||
const isToken = this instanceof Token;
|
const isToken = this instanceof Token;
|
||||||
const ruler = canvas.controls.ruler;
|
const ruler = canvas.controls.ruler
|
||||||
ruler.draggedEntity = this;
|
ruler.draggedEntity = this;
|
||||||
let entityCenter;
|
let entityCenter;
|
||||||
if (
|
if (isToken && canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(this))
|
||||||
isToken &&
|
|
||||||
canvas.grid.isHex &&
|
|
||||||
game.modules.get("hex-size-support")?.active &&
|
|
||||||
CONFIG.hexSizeSupport.getAltSnappingFlag(this)
|
|
||||||
)
|
|
||||||
entityCenter = getHexSizeSupportTokenGridCenter(this);
|
entityCenter = getHexSizeSupportTokenGridCenter(this);
|
||||||
else entityCenter = this.center;
|
else
|
||||||
ruler.rulerOffset = {
|
entityCenter = this.center;
|
||||||
x: entityCenter.x - event.data.origin.x,
|
ruler.rulerOffset = {x: entityCenter.x - event.data.origin.x, y: entityCenter.y - event.data.origin.y};
|
||||||
y: entityCenter.y - event.data.origin.y,
|
|
||||||
};
|
|
||||||
if (game.settings.get(settingsKey, "autoStartMeasurement")) {
|
if (game.settings.get(settingsKey, "autoStartMeasurement")) {
|
||||||
let options = {};
|
let options = {};
|
||||||
setSnapParameterOnOptions(ruler, options);
|
setSnapParameterOnOptions(ruler, options);
|
||||||
@@ -162,42 +152,48 @@ function onEntityLeftDragMoveSnap(wrapped, event) {
|
|||||||
|
|
||||||
function onEntityLeftDragMove(wrapped, event) {
|
function onEntityLeftDragMove(wrapped, event) {
|
||||||
wrapped(event);
|
wrapped(event);
|
||||||
const ruler = canvas.controls.ruler;
|
const ruler = canvas.controls.ruler
|
||||||
if (ruler.isDragRuler) onMouseMove.call(ruler, event);
|
if (ruler.isDragRuler)
|
||||||
|
onMouseMove.call(ruler, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEntityDragLeftDrop(event) {
|
function onEntityDragLeftDrop(event) {
|
||||||
const ruler = canvas.controls.ruler;
|
const ruler = canvas.controls.ruler
|
||||||
if (!ruler.isDragRuler) {
|
if (!ruler.isDragRuler) {
|
||||||
ruler.draggedEntity = undefined;
|
ruler.draggedEntity = undefined;
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
// When we're dragging a measured template no token will ever be selected,
|
// When we're dragging a measured template no token will ever be selected,
|
||||||
// resulting in only the dragged template to be moved as would be expected
|
// resulting in only the dragged template to be moved as would be expected
|
||||||
const selectedTokens = canvas.tokens.controlled;
|
const selectedTokens = canvas.tokens.controlled
|
||||||
// This can happen if the user presses ESC during drag (maybe there are other ways too)
|
// This can happen if the user presses ESC during drag (maybe there are other ways too)
|
||||||
if (selectedTokens.length === 0) selectedTokens.push(ruler.draggedEntity);
|
if (selectedTokens.length === 0)
|
||||||
|
selectedTokens.push(ruler.draggedEntity);
|
||||||
// This can happen if the ruler is being dragged so rapidly that the drag move handler hasn't been called before dropping
|
// This can happen if the ruler is being dragged so rapidly that the drag move handler hasn't been called before dropping
|
||||||
if (ruler._state === Ruler.STATES.STARTING) onMouseMove.call(ruler, event);
|
if (ruler._state === Ruler.STATES.STARTING)
|
||||||
ruler._state = Ruler.STATES.MOVING;
|
onMouseMove.call(ruler, event);
|
||||||
|
ruler._state = Ruler.STATES.MOVING
|
||||||
moveEntities.call(ruler, ruler.draggedEntity, selectedTokens);
|
moveEntities.call(ruler, ruler.draggedEntity, selectedTokens);
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEntityDragLeftCancel(event) {
|
function onEntityDragLeftCancel(event) {
|
||||||
// This function is invoked by right clicking
|
// This function is invoked by right clicking
|
||||||
const ruler = canvas.controls.ruler;
|
const ruler = canvas.controls.ruler
|
||||||
if (!ruler.draggedEntity || ruler._state === Ruler.STATES.MOVING) return false;
|
if (!ruler.draggedEntity || ruler._state === Ruler.STATES.MOVING)
|
||||||
|
return false
|
||||||
|
|
||||||
const rightClickAction = game.settings.get(settingsKey, "rightClickAction");
|
const rightClickAction = game.settings.get(settingsKey, "rightClickAction");
|
||||||
let options = {};
|
let options = {};
|
||||||
setSnapParameterOnOptions(ruler, options);
|
setSnapParameterOnOptions(ruler, options);
|
||||||
|
|
||||||
if (ruler._state === Ruler.STATES.INACTIVE) {
|
if (ruler._state === Ruler.STATES.INACTIVE) {
|
||||||
if (rightClickAction !== RightClickAction.CREATE_WAYPOINT) return false;
|
if (rightClickAction !== RightClickAction.CREATE_WAYPOINT)
|
||||||
|
return false;
|
||||||
ruler.dragRulerStart(options);
|
ruler.dragRulerStart(options);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (ruler._state === Ruler.STATES.MEASURING) {
|
}
|
||||||
|
else if (ruler._state === Ruler.STATES.MEASURING) {
|
||||||
switch (rightClickAction) {
|
switch (rightClickAction) {
|
||||||
case RightClickAction.CREATE_WAYPOINT:
|
case RightClickAction.CREATE_WAYPOINT:
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -216,10 +212,14 @@ function onEntityDragLeftCancel(event) {
|
|||||||
|
|
||||||
function applyGridlessSnapping(event) {
|
function applyGridlessSnapping(event) {
|
||||||
const ruler = canvas.controls.ruler;
|
const ruler = canvas.controls.ruler;
|
||||||
if (!game.settings.get(settingsKey, "useGridlessRaster")) return;
|
if (!game.settings.get(settingsKey, "useGridlessRaster"))
|
||||||
if (!ruler.isDragRuler) return;
|
return;
|
||||||
if (disableSnap) return;
|
if (!ruler.isDragRuler)
|
||||||
if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS) return;
|
return;
|
||||||
|
if (disableSnap)
|
||||||
|
return;
|
||||||
|
if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS)
|
||||||
|
return;
|
||||||
|
|
||||||
const rasterWidth = 35 / canvas.stage.scale.x;
|
const rasterWidth = 35 / canvas.stage.scale.x;
|
||||||
const tokenX = event.data.destination.x;
|
const tokenX = event.data.destination.x;
|
||||||
@@ -229,11 +229,7 @@ function applyGridlessSnapping(event) {
|
|||||||
|
|
||||||
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active;
|
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active;
|
||||||
if (terrainRulerAvailable) {
|
if (terrainRulerAvailable) {
|
||||||
const segments = ruler.constructor
|
const segments = Ruler.dragRulerGetRaysFromWaypoints(ruler.waypoints, destination).map(ray => {return {ray}});
|
||||||
.dragRulerGetRaysFromWaypoints(ruler.waypoints, destination)
|
|
||||||
.map(ray => {
|
|
||||||
return {ray};
|
|
||||||
});
|
|
||||||
const pinpointDistances = new Map();
|
const pinpointDistances = new Map();
|
||||||
for (const range of ranges) {
|
for (const range of ranges) {
|
||||||
pinpointDistances.set(range.range, null);
|
pinpointDistances.set(range.range, null);
|
||||||
@@ -241,7 +237,7 @@ function applyGridlessSnapping(event) {
|
|||||||
terrainRuler.measureDistances(segments, {pinpointDistances});
|
terrainRuler.measureDistances(segments, {pinpointDistances});
|
||||||
const targetDistance = Array.from(pinpointDistances.entries())
|
const targetDistance = Array.from(pinpointDistances.entries())
|
||||||
.filter(([_key, val]) => val)
|
.filter(([_key, val]) => val)
|
||||||
.reduce((value, current) => (value[0] > current[0] ? value : current), [0, null]);
|
.reduce((value, current) => value[0] > current[0] ? value : current, [0, null]);
|
||||||
const rasterLocation = targetDistance[1];
|
const rasterLocation = targetDistance[1];
|
||||||
if (rasterLocation) {
|
if (rasterLocation) {
|
||||||
const deltaX = destination.x - rasterLocation.x;
|
const deltaX = destination.x - rasterLocation.x;
|
||||||
@@ -252,15 +248,12 @@ function applyGridlessSnapping(event) {
|
|||||||
event.data.destination.y = rasterLocation.y - ruler.rulerOffset.y;
|
event.data.destination.y = rasterLocation.y - ruler.rulerOffset.y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
let waypointDistance = 0;
|
let waypointDistance = 0;
|
||||||
let origin = event.data.origin;
|
let origin = event.data.origin;
|
||||||
if (ruler.waypoints.length > 1) {
|
if (ruler.waypoints.length > 1) {
|
||||||
const segments = ruler.constructor
|
const segments = Ruler.dragRulerGetRaysFromWaypoints(ruler.waypoints, destination).map(ray => {return {ray}});
|
||||||
.dragRulerGetRaysFromWaypoints(ruler.waypoints, destination)
|
|
||||||
.map(ray => {
|
|
||||||
return {ray};
|
|
||||||
});
|
|
||||||
origin = segments.pop().ray.A;
|
origin = segments.pop().ray.A;
|
||||||
waypointDistance = canvas.grid.measureDistances(segments).reduce((a, b) => a + b);
|
waypointDistance = canvas.grid.measureDistances(segments).reduce((a, b) => a + b);
|
||||||
origin = {x: origin.x - ruler.rulerOffset.x, y: origin.y - ruler.rulerOffset.y};
|
origin = {x: origin.x - ruler.rulerOffset.x, y: origin.y - ruler.rulerOffset.y};
|
||||||
@@ -273,13 +266,13 @@ function applyGridlessSnapping(event) {
|
|||||||
let targetDistance = ranges
|
let targetDistance = ranges
|
||||||
.map(range => range.range)
|
.map(range => range.range)
|
||||||
.map(range => range - waypointDistance)
|
.map(range => range - waypointDistance)
|
||||||
.map(range => (range * canvas.dimensions.size) / canvas.dimensions.distance)
|
.map(range => range * canvas.dimensions.size / canvas.dimensions.distance)
|
||||||
.filter(range => range < distance)
|
.filter(range => range < distance)
|
||||||
.reduce((a, b) => Math.max(a, b), 0);
|
.reduce((a, b) => Math.max(a, b), 0);
|
||||||
if (targetDistance) {
|
if (targetDistance) {
|
||||||
if (distance < targetDistance + rasterWidth) {
|
if (distance < targetDistance + rasterWidth) {
|
||||||
event.data.destination.x = origin.x + (deltaX * targetDistance) / distance;
|
event.data.destination.x = origin.x + deltaX * targetDistance / distance;
|
||||||
event.data.destination.y = origin.y + (deltaY * targetDistance) / distance;
|
event.data.destination.y = origin.y + deltaY * targetDistance / distance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,26 @@
|
|||||||
import {RightClickAction, settingsKey} from "./settings.js";
|
import {RightClickAction, settingsKey} from "./settings.js"
|
||||||
|
|
||||||
const currentDataVersion = "1.10.0";
|
const currentDataVersion = "1.10.0"
|
||||||
|
|
||||||
export async function performMigrations() {
|
export async function performMigrations() {
|
||||||
if (game.user.isGM) await performWorldMigraionts();
|
if (game.user.isGM)
|
||||||
await performClientMigrations();
|
await performWorldMigraionts()
|
||||||
|
await performClientMigrations()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function performWorldMigraionts() {
|
async function performWorldMigraionts() {
|
||||||
let dataVersion = game.settings.get(settingsKey, "dataVersion");
|
let dataVersion = game.settings.get(settingsKey, "dataVersion");
|
||||||
|
|
||||||
if (dataVersion === currentDataVersion) return;
|
if (dataVersion === currentDataVersion)
|
||||||
|
return;
|
||||||
|
|
||||||
if (dataVersion === "fresh install") {
|
if (dataVersion === "fresh install") {
|
||||||
game.settings.set(settingsKey, "dataVersion", currentDataVersion);
|
game.settings.set(settingsKey, "dataVersion", currentDataVersion)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataVersion === "1.3.0") {
|
if (dataVersion === "1.3.0") {
|
||||||
dataVersion = "1.10.0";
|
dataVersion = "1.10.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
game.settings.set(settingsKey, "dataVersion", dataVersion);
|
game.settings.set(settingsKey, "dataVersion", dataVersion);
|
||||||
@@ -29,15 +31,13 @@ async function performClientMigrations() {
|
|||||||
|
|
||||||
if (dataVersion === "fresh install") {
|
if (dataVersion === "fresh install") {
|
||||||
// Start of migration from unnamed version (< 1.10.0). TODO Remove in a future version
|
// Start of migration from unnamed version (< 1.10.0). TODO Remove in a future version
|
||||||
const swapSpacebarRightClick = game.settings.storage
|
const swapSpacebarRightClick = game.settings.storage.get("client").getItem(`${settingsKey}.swapSpacebarRightClick`);
|
||||||
.get("client")
|
|
||||||
.getItem(`${settingsKey}.swapSpacebarRightClick`);
|
|
||||||
if (swapSpacebarRightClick) {
|
if (swapSpacebarRightClick) {
|
||||||
game.settings.set(settingsKey, "rightClickAction", RightClickAction.CREATE_WAYPOINT);
|
game.settings.set(settingsKey, "rightClickAction", RightClickAction.CREATE_WAYPOINT);
|
||||||
await game.keybindings.set(settingsKey, "createWaypoint", []);
|
await game.keybindings.set(settingsKey, "createWaypoint", []);
|
||||||
await game.keybindings.set(settingsKey, "deleteWaypoint", [{key: "Space"}]);
|
await game.keybindings.set(settingsKey, "deleteWaypoint", [{key: "Space"}]);
|
||||||
}
|
}
|
||||||
// End of migration from unnamed version
|
// End of migration from unnamed version
|
||||||
game.settings.set(settingsKey, "clientDataVersion", currentDataVersion);
|
game.settings.set(settingsKey, "clientDataVersion", currentDataVersion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,35 +9,38 @@ function initTrackingFlag(combatant) {
|
|||||||
if (isNaN(dragRulerFlag.trackedRound)) {
|
if (isNaN(dragRulerFlag.trackedRound)) {
|
||||||
mergeObject(dragRulerFlag, initialFlag);
|
mergeObject(dragRulerFlag, initialFlag);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
combatant.data.flags.dragRuler = initialFlag;
|
combatant.data.flags.dragRuler = initialFlag;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitializedCombatant(token, combat) {
|
function getInitializedCombatant(token, combat) {
|
||||||
const combatant = combat.getCombatantByToken(token.id);
|
const combatant = combat.getCombatantByToken(token.id);
|
||||||
if (!combatant) return undefined;
|
if (!combatant)
|
||||||
|
return undefined;
|
||||||
initTrackingFlag(combatant);
|
initTrackingFlag(combatant);
|
||||||
return combatant;
|
return combatant;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function trackRays(tokens, tokenRays) {
|
export async function trackRays(tokens, tokenRays) {
|
||||||
const combat = game.combat;
|
const combat = game.combat;
|
||||||
if (!combat) return;
|
if (!combat)
|
||||||
if (!combat.started) return;
|
return;
|
||||||
|
if (!combat.started)
|
||||||
|
return;
|
||||||
if (!(tokens instanceof Array)) {
|
if (!(tokens instanceof Array)) {
|
||||||
tokens = [tokens];
|
tokens = [tokens];
|
||||||
tokenRays = [tokenRays];
|
tokenRays = [tokenRays];
|
||||||
}
|
}
|
||||||
const updates = Array.from(zip(tokens, tokenRays))
|
const updates = Array.from(zip(tokens, tokenRays)).map(([token, rays]) => calculateUpdate(combat, token, rays)).filter(Boolean);
|
||||||
.map(([token, rays]) => calculateUpdate(combat, token, rays))
|
|
||||||
.filter(Boolean);
|
|
||||||
await updateCombatantDragRulerFlags(combat, updates);
|
await updateCombatantDragRulerFlags(combat, updates);
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateUpdate(combat, token, rays) {
|
function calculateUpdate(combat, token, rays) {
|
||||||
const combatant = getInitializedCombatant(token, combat);
|
const combatant = getInitializedCombatant(token, combat);
|
||||||
if (!combatant) return;
|
if (!combatant)
|
||||||
|
return;
|
||||||
|
|
||||||
// Check if we have entered a new round. If so, remove the currently stored path
|
// Check if we have entered a new round. If so, remove the currently stored path
|
||||||
if (combat.data.round > combatant.data.flags.dragRuler.trackedRound) {
|
if (combat.data.round > combatant.data.flags.dragRuler.trackedRound) {
|
||||||
@@ -53,10 +56,7 @@ function calculateUpdate(combat, token, rays) {
|
|||||||
// Ignore rays that have the same start and end coordinates
|
// Ignore rays that have the same start and end coordinates
|
||||||
if (ray.A.x !== ray.B.x || ray.A.y !== ray.B.y) {
|
if (ray.A.x !== ray.B.x || ray.A.y !== ray.B.y) {
|
||||||
if (terrainRulerAvailable) {
|
if (terrainRulerAvailable) {
|
||||||
measureDistances([{ray}], token, getTokenShape(token), {
|
measureDistances([{ray}], token, getTokenShape(token), {terrainRulerInitialState: waypoints[waypoints.length - 1]?.dragRulerFinalState, enableTerrainRuler: terrainRulerAvailable});
|
||||||
terrainRulerInitialState: waypoints[waypoints.length - 1]?.dragRulerFinalState,
|
|
||||||
enableTerrainRuler: terrainRulerAvailable,
|
|
||||||
});
|
|
||||||
ray.A.dragRulerVisitedSpaces = ray.terrainRulerVisitedSpaces;
|
ray.A.dragRulerVisitedSpaces = ray.terrainRulerVisitedSpaces;
|
||||||
ray.A.dragRulerFinalState = ray.terrainRulerFinalState;
|
ray.A.dragRulerFinalState = ray.terrainRulerFinalState;
|
||||||
}
|
}
|
||||||
@@ -68,18 +68,23 @@ function calculateUpdate(combat, token, rays) {
|
|||||||
|
|
||||||
export function getMovementHistory(token) {
|
export function getMovementHistory(token) {
|
||||||
const combat = game.combat;
|
const combat = game.combat;
|
||||||
if (!combat) return [];
|
if (!combat)
|
||||||
|
return [];
|
||||||
const combatant = combat.getCombatantByToken(token.id);
|
const combatant = combat.getCombatantByToken(token.id);
|
||||||
if (!combatant) return [];
|
if (!combatant)
|
||||||
const dragRulerFlags = combatant.flags.dragRuler;
|
return [];
|
||||||
if (!dragRulerFlags) return [];
|
const dragRulerFlags = combatant.data.flags.dragRuler;
|
||||||
if (combat.round > dragRulerFlags.trackedRound) return [];
|
if (!dragRulerFlags)
|
||||||
|
return [];
|
||||||
|
if (combat.data.round > dragRulerFlags.trackedRound)
|
||||||
|
return [];
|
||||||
return dragRulerFlags.passedWaypoints ?? [];
|
return dragRulerFlags.passedWaypoints ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeLastHistoryEntryIfAt(token, x, y) {
|
export async function removeLastHistoryEntryIfAt(token, x, y) {
|
||||||
const history = getMovementHistory(token);
|
const history = getMovementHistory(token);
|
||||||
if (history.length === 0) return;
|
if (history.length === 0)
|
||||||
|
return;
|
||||||
const entry = history[history.length - 1];
|
const entry = history[history.length - 1];
|
||||||
if (!isClose(x + token.w / 2, entry.x, 0.1) || !isClose(y + token.h / 2, entry.y, 0.1)) {
|
if (!isClose(x + token.w / 2, entry.x, 0.1) || !isClose(y + token.h / 2, entry.y, 0.1)) {
|
||||||
return;
|
return;
|
||||||
@@ -87,15 +92,14 @@ export async function removeLastHistoryEntryIfAt(token, x, y) {
|
|||||||
history.pop();
|
history.pop();
|
||||||
const combat = game.combat;
|
const combat = game.combat;
|
||||||
const combatant = combat.getCombatantByToken(token.id);
|
const combatant = combat.getCombatantByToken(token.id);
|
||||||
await updateCombatantDragRulerFlags(combat, [
|
await updateCombatantDragRulerFlags(combat, [{_id: combatant.id, dragRulerFlags: combatant.data.flags.dragRuler}]);
|
||||||
{_id: combatant.id, dragRulerFlags: combatant.data.flags.dragRuler},
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resetMovementHistory(combat, combatantId) {
|
export async function resetMovementHistory(combat, combatantId) {
|
||||||
const combatant = combat.combatants.get(combatantId);
|
const combatant = combat.combatants.get(combatantId);
|
||||||
const dragRulerFlags = combatant.data.flags.dragRuler;
|
const dragRulerFlags = combatant.data.flags.dragRuler;
|
||||||
if (!dragRulerFlags) return;
|
if (!dragRulerFlags)
|
||||||
|
return;
|
||||||
dragRulerFlags.passedWaypoints = null;
|
dragRulerFlags.passedWaypoints = null;
|
||||||
dragRulerFlags.trackedRound = null;
|
dragRulerFlags.trackedRound = null;
|
||||||
dragRulerFlags.rulerState = null;
|
dragRulerFlags.rulerState = null;
|
||||||
@@ -0,0 +1,514 @@
|
|||||||
|
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, 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) {
|
||||||
|
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 {
|
||||||
|
static maxCacheLayers = 5;
|
||||||
|
static maxBackgroundCachingMillis = 10;
|
||||||
|
static maxAnimationCachingMillis = 5;
|
||||||
|
static backgroundCachingTimeoutMillis = 200;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.layers = new Map();
|
||||||
|
this.background = {
|
||||||
|
nextJobId: null,
|
||||||
|
nextTimeoutId: null,
|
||||||
|
nextAnimationFrameId: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.layers.clear();
|
||||||
|
if (this.background.nextJobId) {
|
||||||
|
window.cancelIdleCallback(this.background.nextJobId);
|
||||||
|
this.background.nextJobId = null;
|
||||||
|
}
|
||||||
|
this.cancelTimeout();
|
||||||
|
this.cancelAnimationFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the cache layer for this token, using information that can make a difference to the pathfinding algorithm
|
||||||
|
* If a layer that suits this token doesn't exist, create one
|
||||||
|
*/
|
||||||
|
getCacheLayer(token) {
|
||||||
|
const tokenData = buildTokenData(token);
|
||||||
|
// 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) {
|
||||||
|
// Check if we already have the max number of layers. If we do,
|
||||||
|
// get rid of the one that hasn't been used for the longest
|
||||||
|
if (this.layers.size >= Cache.maxCacheLayers) {
|
||||||
|
const oldestCache = Array.from(this.layers.values())
|
||||||
|
.reduce((layer1, layer2) => (layer1?.lastUsed < layer2.lastUsed) ? layer1 : layer2, null);
|
||||||
|
this.layers.delete(oldestCache.cacheId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the new cache
|
||||||
|
cacheLayer = new CacheLayer(tokenData, cacheId);
|
||||||
|
this.layers.set(cacheId, cacheLayer);
|
||||||
|
} else {
|
||||||
|
// Register that we're using this cache right now
|
||||||
|
cacheLayer.registerUse();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache = new Cache();
|
||||||
|
|
||||||
|
let use5105 = false;
|
||||||
|
let gridlessPathfinders = new Map();
|
||||||
|
let gridWidth, gridHeight;
|
||||||
|
|
||||||
|
export function isPathfindingEnabled() {
|
||||||
|
if (this.user !== game.user)
|
||||||
|
return false;
|
||||||
|
if (!game.user.isGM && !game.settings.get(settingsKey, "allowPathfinding"))
|
||||||
|
return false;
|
||||||
|
if (moveWithoutAnimation)
|
||||||
|
return false;
|
||||||
|
return game.settings.get(settingsKey, "autoPathfinding") != togglePathfinding;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findPath(from, to, token, previousWaypoints) {
|
||||||
|
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);
|
||||||
|
if (!pathfinder) {
|
||||||
|
let radiusMultiplier = game.settings.get(settingsKey, "pathfindingRadius");
|
||||||
|
// TODO Pass proper options to listAllTerrain
|
||||||
|
// TODO Wipe caches when they become invalid
|
||||||
|
// TODO Multiple caches, analog to gridded pathfinding
|
||||||
|
pathfinder = GridlessPathfinding.initialize(canvas.walls.placeables, canvas.terrain.listAllTerrain(), tokenSize * radiusMultiplier, token.data.elevation, Boolean(game.modules.get("wall-height")?.active));
|
||||||
|
gridlessPathfinders.set(tokenSize, pathfinder);
|
||||||
|
}
|
||||||
|
paintGridlessPathfindingDebug(pathfinder);
|
||||||
|
return GridlessPathfinding.findPath(pathfinder, from, to);
|
||||||
|
} else {
|
||||||
|
const cacheLayer = cache.getCacheLayer(token);
|
||||||
|
const firstNode = calculatePath(from, to, cacheLayer, previousWaypoints);
|
||||||
|
if (!firstNode)
|
||||||
|
return null;
|
||||||
|
paintGriddedPathfindingDebug(firstNode, cacheLayer.tokenData);
|
||||||
|
const path = [];
|
||||||
|
let currentNode = firstNode;
|
||||||
|
while (currentNode) {
|
||||||
|
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
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function terrainRulerWrapper(from, to) {
|
||||||
|
// TODO Send list of terrain to terrain layer
|
||||||
|
const ray = new Ray(from, to);
|
||||||
|
return terrainRuler.measureDistances([{ray}])[0] / canvas.dimensions.distance * canvas.dimensions.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTokenData(token) {
|
||||||
|
// Almost all the information we need is for calculating the snap point
|
||||||
|
const tokenData = buildSnapPointTokenData(token);
|
||||||
|
|
||||||
|
// If Wall Height is enabled, which walls matter depends on the token's elevation.
|
||||||
|
// Depending on the settings in Wall Height, the height we care about is either their
|
||||||
|
// foot height (elevation) or eye height (losHeight).
|
||||||
|
if (isModuleActive("wall-height")) {
|
||||||
|
const blockSightMovement = game.settings.get("wall-height", "blockSightMovement");
|
||||||
|
tokenData.elevation = blockSightMovement ? token.losHeight : token.data.elevation;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNode(pos, cacheLayer, initialize = true) {
|
||||||
|
const node = cacheLayer.nodes[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) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
node.edges.push({target: neighbor, cost: edgeCost});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculatePath(from, to, cacheLayer, previousWaypoints) {
|
||||||
|
use5105 = game.system.id === "pf2e" || canvas.grid.diagonalRule === "5105";
|
||||||
|
let startCost = 0;
|
||||||
|
if (use5105 && canvas.grid.type === CONST.GRID_TYPES.SQUARE) {
|
||||||
|
previousWaypoints = previousWaypoints.map(w => getGridPositionFromPixelsObj(w));
|
||||||
|
startCost = (calcNoDiagonals(previousWaypoints) % 2) * 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextNodes = new PriorityQueueSet((node1, node2) => node1.node === node2.node, node => node.estimated);
|
||||||
|
const previousNodes = new Set();
|
||||||
|
|
||||||
|
nextNodes.pushWithPriority(
|
||||||
|
{
|
||||||
|
node: getNode(from, cacheLayer),
|
||||||
|
cost: startCost,
|
||||||
|
estimated: startCost + estimateCost(from, to),
|
||||||
|
previous: null
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
while (nextNodes.hasNext()) {
|
||||||
|
// Get node with cheapest estimate
|
||||||
|
const currentNode = nextNodes.pop();
|
||||||
|
if (currentNode.node.x === to.x && currentNode.node.y === to.y) {
|
||||||
|
return buildPathNodes(currentNode);
|
||||||
|
}
|
||||||
|
previousNodes.add(currentNode.node);
|
||||||
|
for (const edge of currentNode.node.edges) {
|
||||||
|
const neighborNode = getNode(edge.target, cacheLayer);
|
||||||
|
if (previousNodes.has(neighborNode)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const neighbor = {
|
||||||
|
node: neighborNode,
|
||||||
|
cost: currentNode.cost + edge.cost,
|
||||||
|
estimated: currentNode.cost + edge.cost + estimateCost(neighborNode, to),
|
||||||
|
previous: currentNode
|
||||||
|
};
|
||||||
|
nextNodes.pushWithPriority(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
let diagonals = 0;
|
||||||
|
for (const [p1, p2] of iterPairs(waypoints)) {
|
||||||
|
diagonals += Math.min(Math.abs(p1.x - p2.x), Math.abs(p1.y - p2.y));
|
||||||
|
}
|
||||||
|
return diagonals;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimate the travel distance between two points, as the crow flies. Most of the time, this is 1
|
||||||
|
* per space, but for a square grid using 5-10-5 diagonals, count each diagonal as an extra 0.5
|
||||||
|
*/
|
||||||
|
function estimateCost(pos, target) {
|
||||||
|
const distX = Math.abs(pos.x - target.x);
|
||||||
|
const distY = Math.abs(pos.y - target.y);
|
||||||
|
return Math.max(distX, distY) + (use5105 ? Math.min(distX, distY) * 0.5 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stepCollidesWithWall(from, to, tokenData) {
|
||||||
|
const stepStart = getSnapPointForTokenDataObj(getPixelsFromGridPositionObj(from), tokenData);
|
||||||
|
const stepEnd = getSnapPointForTokenDataObj(getPixelsFromGridPositionObj(to), tokenData);
|
||||||
|
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() {
|
||||||
|
cache.clear();
|
||||||
|
for (const pathfinder of gridlessPathfinders.values()) {
|
||||||
|
GridlessPathfinding.free(pathfinder);
|
||||||
|
}
|
||||||
|
gridlessPathfinders.clear();
|
||||||
|
if (debugGraphics)
|
||||||
|
debugGraphics.removeChildren().forEach(c => c.destroy());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initializePathfinding() {
|
||||||
|
gridWidth = Math.ceil(canvas.dimensions.width / canvas.grid.w);
|
||||||
|
gridHeight = Math.ceil(canvas.dimensions.height / canvas.grid.h);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startBackgroundCaching(token) {
|
||||||
|
// Background caching isn't yet supported for gridless scenes
|
||||||
|
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS)
|
||||||
|
return;
|
||||||
|
if (game.user.isGM || game.settings.get(settingsKey, "allowPathfinding")) {
|
||||||
|
cache.startBackgroundCaching(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintGriddedPathfindingDebug(firstNode, tokenData) {
|
||||||
|
if (!CONFIG.debug.dragRuler)
|
||||||
|
return;
|
||||||
|
|
||||||
|
debugGraphics.removeChildren().forEach(c => c.destroy());
|
||||||
|
let currentNode = firstNode;
|
||||||
|
while (currentNode) {
|
||||||
|
let text = new PIXI.Text(currentNode.cost.toFixed(1));
|
||||||
|
let pixels = getSnapPointForTokenDataObj(getPixelsFromGridPositionObj(currentNode.node), tokenData);
|
||||||
|
text.anchor.set(0.5, 1.0);
|
||||||
|
text.x = pixels.x;
|
||||||
|
text.y = pixels.y;
|
||||||
|
debugGraphics.addChild(text);
|
||||||
|
currentNode = currentNode.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintGridlessPathfindingDebug(pathfinder) {
|
||||||
|
if (!CONFIG.debug.dragRuler)
|
||||||
|
return;
|
||||||
|
|
||||||
|
debugGraphics.removeChildren().forEach(c => c.destroy());
|
||||||
|
let graphic = new PIXI.Graphics();
|
||||||
|
graphic.lineStyle(2, 0x440000);
|
||||||
|
for (const point of GridlessPathfinding.debugGetPathfindingPoints(pathfinder)) {
|
||||||
|
graphic.drawCircle(point.x, point.y, 5);
|
||||||
|
}
|
||||||
|
debugGraphics.addChild(graphic);
|
||||||
|
}
|
||||||
+215
@@ -0,0 +1,215 @@
|
|||||||
|
import {currentSpeedProvider, getColorForDistanceAndToken, getRangesFromSpeedProvider} from "./api.js";
|
||||||
|
import {getHexSizeSupportTokenGridCenter} from "./compatibility.js";
|
||||||
|
import {cancelScheduledMeasurement, measure} from "./foundry_imports.js"
|
||||||
|
import {getMovementHistory} from "./movement_tracking.js";
|
||||||
|
import {settingsKey} from "./settings.js";
|
||||||
|
import {getSnapPointForEntity} from "./util.js";
|
||||||
|
|
||||||
|
export function extendRuler() {
|
||||||
|
class DragRulerRuler extends Ruler {
|
||||||
|
// Functions below are overridden versions of functions in Ruler
|
||||||
|
constructor(user, {color=null}={}) {
|
||||||
|
super(user, {color});
|
||||||
|
this.previousWaypoints = [];
|
||||||
|
this.previousLabels = this.addChild(new PIXI.Container());
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
super.clear();
|
||||||
|
this.previousWaypoints = [];
|
||||||
|
this.previousLabels.removeChildren().forEach(c => c.destroy());
|
||||||
|
this.dragRulerRanges = undefined;
|
||||||
|
cancelScheduledMeasurement.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveToken(event) {
|
||||||
|
// Disable moveToken if Drag Ruler is active
|
||||||
|
if (!this.isDragRuler)
|
||||||
|
return await super.moveToken(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
const json = super.toJSON();
|
||||||
|
if (this.draggedEntity) {
|
||||||
|
const isToken = this.draggedEntity instanceof Token;
|
||||||
|
json.draggedEntityIsToken = isToken;
|
||||||
|
json.draggedEntity = this.draggedEntity.id;
|
||||||
|
json.waypoints = json.waypoints.map(old => {
|
||||||
|
let w = duplicate(old);
|
||||||
|
w.isPathfinding = undefined;
|
||||||
|
return w;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(data) {
|
||||||
|
// Don't show a GMs drag ruler to non GM players
|
||||||
|
if (data.draggedEntity && this.user.isGM && !game.user.isGM && !game.settings.get(settingsKey, "showGMRulerToPlayers"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (data.draggedEntity) {
|
||||||
|
if (data.draggedEntityIsToken)
|
||||||
|
this.draggedEntity = canvas.tokens.get(data.draggedEntity);
|
||||||
|
else
|
||||||
|
this.draggedEntity = canvas.templates.get(data.draggedEntity);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.draggedEntity = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
super.update(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
measure(destination, options={}) {
|
||||||
|
if (this.isDragRuler) {
|
||||||
|
// If this is the ruler of a remote user take the waypoints as they were transmitted and don't apply any additional snapping to them
|
||||||
|
if (this.user !== game.user)
|
||||||
|
options.snap = false;
|
||||||
|
return measure.call(this, destination, options);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return super.measure(destination, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_endMeasurement() {
|
||||||
|
super._endMeasurement();
|
||||||
|
this.draggedEntity = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The functions below aren't present in the orignal Ruler class and are added by Drag Ruler
|
||||||
|
dragRulerAddWaypoint(point, options={}) {
|
||||||
|
options.snap = options.snap ?? true;
|
||||||
|
if (options.snap) {
|
||||||
|
point = getSnapPointForEntity(point.x, point.y, this.draggedEntity);
|
||||||
|
}
|
||||||
|
this.waypoints.push(new PIXI.Point(point.x, point.y));
|
||||||
|
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
|
||||||
|
this.waypoints.filter(waypoint => waypoint.isPathfinding).forEach(waypoint => waypoint.isPathfinding = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
dragRulerAddWaypointHistory(waypoints) {
|
||||||
|
waypoints.forEach(waypoint => waypoint.isPrevious = true);
|
||||||
|
this.waypoints = this.waypoints.concat(waypoints);
|
||||||
|
for (const waypoint of waypoints) {
|
||||||
|
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dragRulerClearWaypoints() {
|
||||||
|
this.waypoints = [];
|
||||||
|
this.labels.removeChildren().forEach(c => c.destroy());
|
||||||
|
}
|
||||||
|
|
||||||
|
dragRulerDeleteWaypoint(event={preventDefault: () => {return}}, options={}) {
|
||||||
|
this.dragRulerRemovePathfindingWaypoints();
|
||||||
|
options.snap = options.snap ?? true;
|
||||||
|
if (this.waypoints.filter(w => !w.isPrevious).length > 1) {
|
||||||
|
event.preventDefault();
|
||||||
|
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens);
|
||||||
|
const rulerOffset = this.rulerOffset;
|
||||||
|
|
||||||
|
// Options are not passed to _removeWaypoint in vanilla Foundry.
|
||||||
|
// Send them in case other modules have overriden that behavior and accept an options parameter (Toggle Snap to Grid)
|
||||||
|
this._removeWaypoint({x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y}, options);
|
||||||
|
game.user.broadcastActivity({ruler: this});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.dragRulerAbortDrag(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dragRulerRemovePathfindingWaypoints() {
|
||||||
|
this.waypoints.filter(waypoint => waypoint.isPathfinding).forEach(_ => this.labels.removeChild(this.labels.children[this.labels.children.length - 1]).destroy());
|
||||||
|
this.waypoints = this.waypoints.filter(waypoint => !waypoint.isPathfinding);
|
||||||
|
}
|
||||||
|
|
||||||
|
dragRulerAbortDrag(event={preventDefault: () => {return}}) {
|
||||||
|
const token = this.draggedEntity;
|
||||||
|
this._endMeasurement();
|
||||||
|
|
||||||
|
// Deactivate the drag workflow in mouse
|
||||||
|
token.mouseInteractionManager._deactivateDragEvents();
|
||||||
|
token.mouseInteractionManager.state = token.mouseInteractionManager.states.HOVER;
|
||||||
|
|
||||||
|
// This will cancel the current drag operation
|
||||||
|
// Pass in a fake event that hopefully is enough to allow other modules to function
|
||||||
|
token._onDragLeftCancel(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
async dragRulerRecalculate(tokenIds) {
|
||||||
|
if (this._state !== Ruler.STATES.MEASURING)
|
||||||
|
return;
|
||||||
|
if (tokenIds && !tokenIds.includes(this.draggedEntity.id))
|
||||||
|
return;
|
||||||
|
const waypoints = this.waypoints.filter(waypoint => !waypoint.isPrevious);
|
||||||
|
this.dragRulerClearWaypoints();
|
||||||
|
if (game.settings.get(settingsKey, "enableMovementHistory"))
|
||||||
|
this.dragRulerAddWaypointHistory(getMovementHistory(this.draggedEntity));
|
||||||
|
for (const waypoint of waypoints) {
|
||||||
|
this.dragRulerAddWaypoint(waypoint, {snap: false});
|
||||||
|
}
|
||||||
|
this.measure(this.destination);
|
||||||
|
game.user.broadcastActivity({ruler: this});
|
||||||
|
}
|
||||||
|
|
||||||
|
static dragRulerGetRaysFromWaypoints(waypoints, destination) {
|
||||||
|
if ( destination )
|
||||||
|
waypoints = waypoints.concat([destination]);
|
||||||
|
return waypoints.slice(1).map((wp, i) => {
|
||||||
|
const ray = new Ray(waypoints[i], wp);
|
||||||
|
ray.isPrevious = Boolean(waypoints[i].isPrevious);
|
||||||
|
return ray;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dragRulerGetColorForDistance(distance) {
|
||||||
|
if (!this.isDragRuler)
|
||||||
|
return this.color;
|
||||||
|
if (!this.draggedEntity.actor) {
|
||||||
|
return this.color;
|
||||||
|
}
|
||||||
|
// Don't apply colors if the current user doesn't have at least observer permissions
|
||||||
|
if (this.draggedEntity.actor.permission < 2) {
|
||||||
|
// If this is a pc and alwaysShowSpeedForPCs is enabled we show the color anyway
|
||||||
|
if (!(this.draggedEntity.actor.data.type === "character" && game.settings.get(settingsKey, "alwaysShowSpeedForPCs")))
|
||||||
|
return this.color;
|
||||||
|
}
|
||||||
|
distance = Math.round(distance * 100) / 100;
|
||||||
|
if (!this.dragRulerRanges)
|
||||||
|
this.dragRulerRanges = getRangesFromSpeedProvider(this.draggedEntity);
|
||||||
|
return getColorForDistanceAndToken(distance, this.draggedEntity, this.dragRulerRanges) ?? this.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragRulerStart(options, measureImmediately=true) {
|
||||||
|
const entity = this.draggedEntity;
|
||||||
|
const isToken = entity instanceof Token;
|
||||||
|
if (isToken && !currentSpeedProvider.usesRuler(entity))
|
||||||
|
return;
|
||||||
|
const ruler = canvas.controls.ruler;
|
||||||
|
ruler.clear();
|
||||||
|
ruler._state = Ruler.STATES.STARTING;
|
||||||
|
let entityCenter;
|
||||||
|
if (isToken && canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(entity))
|
||||||
|
entityCenter = getHexSizeSupportTokenGridCenter(entity);
|
||||||
|
else
|
||||||
|
entityCenter = entity.center;
|
||||||
|
if (isToken && game.settings.get(settingsKey, "enableMovementHistory"))
|
||||||
|
ruler.dragRulerAddWaypointHistory(getMovementHistory(entity));
|
||||||
|
ruler.dragRulerAddWaypoint(entityCenter, {snap: false});
|
||||||
|
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens);
|
||||||
|
const destination = {x: mousePosition.x + ruler.rulerOffset.x, y: mousePosition.y + ruler.rulerOffset.y};
|
||||||
|
if (measureImmediately)
|
||||||
|
ruler.measure(destination, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
dragRulerSendState() {
|
||||||
|
game.user.broadcastActivity({
|
||||||
|
ruler: this.toJSON()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ruler = DragRulerRuler;
|
||||||
|
}
|
||||||
+122
-125
@@ -1,18 +1,14 @@
|
|||||||
import {
|
import {availableSpeedProviders, currentSpeedProvider, getDefaultSpeedProvider, updateSpeedProvider} from "./api.js";
|
||||||
availableSpeedProviders,
|
import {SpeedProvider} from "./speed_provider.js"
|
||||||
currentSpeedProvider,
|
import {wipePathfindingCache} from "./pathfinding.js"
|
||||||
getDefaultSpeedProvider,
|
import { early_isGM } from "./util.js";
|
||||||
updateSpeedProvider,
|
|
||||||
} from "./api.js";
|
|
||||||
import {SpeedProvider} from "./speed_provider.js";
|
|
||||||
import {early_isGM} from "./util.js";
|
|
||||||
|
|
||||||
export const settingsKey = "drag-ruler";
|
export const settingsKey = "drag-ruler";
|
||||||
|
|
||||||
export const RightClickAction = Object.freeze({
|
export const RightClickAction = Object.freeze({
|
||||||
CREATE_WAYPOINT: 0,
|
CREATE_WAYPOINT: 0,
|
||||||
DELETE_WAYPOINT: 1,
|
DELETE_WAYPOINT: 1,
|
||||||
ABORT_DRAG: 2,
|
ABORT_DRAG:2,
|
||||||
});
|
});
|
||||||
|
|
||||||
function delayedReload() {
|
function delayedReload() {
|
||||||
@@ -24,14 +20,14 @@ export function registerSettings() {
|
|||||||
scope: "world",
|
scope: "world",
|
||||||
config: false,
|
config: false,
|
||||||
type: String,
|
type: String,
|
||||||
default: "fresh install",
|
default: "fresh install"
|
||||||
});
|
})
|
||||||
|
|
||||||
game.settings.register(settingsKey, "clientDataVersion", {
|
game.settings.register(settingsKey, "clientDataVersion", {
|
||||||
scope: "client",
|
scope: "client",
|
||||||
config: false,
|
config: false,
|
||||||
type: String,
|
type: String,
|
||||||
default: "fresh install",
|
default: "fresh install"
|
||||||
});
|
});
|
||||||
|
|
||||||
game.settings.register(settingsKey, "rightClickAction", {
|
game.settings.register(settingsKey, "rightClickAction", {
|
||||||
@@ -72,7 +68,7 @@ export function registerSettings() {
|
|||||||
config: true,
|
config: true,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
game.settings.register(settingsKey, "showGMRulerToPlayers", {
|
game.settings.register(settingsKey, "showGMRulerToPlayers", {
|
||||||
name: "drag-ruler.settings.showGMRulerToPlayers.name",
|
name: "drag-ruler.settings.showGMRulerToPlayers.name",
|
||||||
@@ -81,7 +77,7 @@ export function registerSettings() {
|
|||||||
config: true,
|
config: true,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
game.settings.register(settingsKey, "enableMovementHistory", {
|
game.settings.register(settingsKey, "enableMovementHistory", {
|
||||||
name: "drag-ruler.settings.enableMovementHistory.name",
|
name: "drag-ruler.settings.enableMovementHistory.name",
|
||||||
@@ -92,26 +88,32 @@ export function registerSettings() {
|
|||||||
default: true,
|
default: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (game.modules.get("routinglib")?.active) {
|
game.settings.register(settingsKey, "allowPathfinding", {
|
||||||
game.settings.register(settingsKey, "allowPathfinding", {
|
name: "drag-ruler.settings.allowPathfinding.name",
|
||||||
name: "drag-ruler.settings.allowPathfinding.name",
|
hint: "drag-ruler.settings.allowPathfinding.hint",
|
||||||
hint: "drag-ruler.settings.allowPathfinding.hint",
|
scope: "world",
|
||||||
scope: "world",
|
config: true,
|
||||||
config: true,
|
type: Boolean,
|
||||||
type: Boolean,
|
default: false,
|
||||||
default: false,
|
onChange: delayedReload,
|
||||||
onChange: delayedReload,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
game.settings.register(settingsKey, "autoPathfinding", {
|
game.settings.register(settingsKey, "autoPathfinding", {
|
||||||
name: "drag-ruler.settings.autoPathfinding.name",
|
name: "drag-ruler.settings.autoPathfinding.name",
|
||||||
hint: "drag-ruler.settings.autoPathfinding.hint",
|
hint: "drag-ruler.settings.autoPathfinding.hint",
|
||||||
scope: "client",
|
scope: "client",
|
||||||
config: early_isGM() || game.settings.get(settingsKey, "allowPathfinding"),
|
config: early_isGM() || game.settings.get(settingsKey, "allowPathfinding"),
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
game.settings.register(settingsKey, "pathfindingRadius", {
|
||||||
|
scope: "world",
|
||||||
|
config: false,
|
||||||
|
type: Number,
|
||||||
|
default: 0.9,
|
||||||
|
onChange: wipePathfindingCache,
|
||||||
|
});
|
||||||
|
|
||||||
game.settings.register(settingsKey, "lastTerrainRulerHintTime", {
|
game.settings.register(settingsKey, "lastTerrainRulerHintTime", {
|
||||||
config: false,
|
config: false,
|
||||||
@@ -132,7 +134,7 @@ export function registerSettings() {
|
|||||||
type: String,
|
type: String,
|
||||||
default: getDefaultSpeedProvider(),
|
default: getDefaultSpeedProvider(),
|
||||||
onChange: updateSpeedProvider,
|
onChange: updateSpeedProvider,
|
||||||
});
|
})
|
||||||
|
|
||||||
game.settings.registerMenu(settingsKey, "speedProviderSettings", {
|
game.settings.registerMenu(settingsKey, "speedProviderSettings", {
|
||||||
name: "drag-ruler.settings.speedProviderSettings.name",
|
name: "drag-ruler.settings.speedProviderSettings.name",
|
||||||
@@ -141,7 +143,7 @@ export function registerSettings() {
|
|||||||
icon: "fas fa-tachometer-alt",
|
icon: "fas fa-tachometer-alt",
|
||||||
type: SpeedProviderSettings,
|
type: SpeedProviderSettings,
|
||||||
restricted: false,
|
restricted: false,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
class SpeedProviderSettings extends FormApplication {
|
class SpeedProviderSettings extends FormApplication {
|
||||||
@@ -151,44 +153,43 @@ class SpeedProviderSettings extends FormApplication {
|
|||||||
title: game.i18n.localize("drag-ruler.settings.speedProviderSettings.windowTitle"),
|
title: game.i18n.localize("drag-ruler.settings.speedProviderSettings.windowTitle"),
|
||||||
template: "modules/drag-ruler/templates/speed_provider_settings.html",
|
template: "modules/drag-ruler/templates/speed_provider_settings.html",
|
||||||
width: 600,
|
width: 600,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getData(options = {}) {
|
getData(options={}) {
|
||||||
const data = {};
|
const data = {}
|
||||||
data.isGM = game.user.isGM;
|
data.isGM = game.user.isGM
|
||||||
const selectedProvider = currentSpeedProvider.id;
|
const selectedProvider = currentSpeedProvider.id
|
||||||
|
|
||||||
// Insert all speed providers into the template data
|
// Insert all speed providers into the template data
|
||||||
data.providers = Object.values(availableSpeedProviders).map(speedProvider => {
|
data.providers = Object.values(availableSpeedProviders).map(speedProvider => {
|
||||||
const provider = {};
|
const provider = {}
|
||||||
provider.id = speedProvider.id;
|
provider.id = speedProvider.id
|
||||||
provider.hasSettings = speedProvider instanceof SpeedProvider;
|
provider.hasSettings = speedProvider instanceof SpeedProvider
|
||||||
if (provider.hasSettings) provider.settings = enumerateProviderSettings(speedProvider);
|
if (provider.hasSettings)
|
||||||
let dotPosition = provider.id.indexOf(".");
|
provider.settings = enumerateProviderSettings(speedProvider)
|
||||||
if (dotPosition === -1) dotPosition = provider.id.length;
|
let dotPosition = provider.id.indexOf(".")
|
||||||
const type = provider.id.substring(0, dotPosition);
|
if (dotPosition === -1)
|
||||||
const id = provider.id.substring(dotPosition + 1);
|
dotPosition = provider.id.length
|
||||||
|
const type = provider.id.substring(0, dotPosition)
|
||||||
|
const id = provider.id.substring(dotPosition + 1)
|
||||||
if (type === "native") {
|
if (type === "native") {
|
||||||
provider.selectTitle = game.i18n.localize(
|
provider.selectTitle = game.i18n.localize("drag-ruler.settings.speedProviderSettings.speedProvider.choices.native")
|
||||||
"drag-ruler.settings.speedProviderSettings.speedProvider.choices.native",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
let name;
|
|
||||||
if (type === "module") {
|
|
||||||
name = game.modules.get(id).data.title;
|
|
||||||
} else {
|
|
||||||
name = game.system.data.title;
|
|
||||||
}
|
|
||||||
provider.selectTitle = game.i18n.format(
|
|
||||||
`drag-ruler.settings.speedProviderSettings.speedProvider.choices.${type}`,
|
|
||||||
{name},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
provider.isSelected = provider.id === selectedProvider;
|
else {
|
||||||
return provider;
|
let name
|
||||||
});
|
if (type === "module") {
|
||||||
data.selectedProviderName = data.providers.find(provider => provider.isSelected).selectTitle;
|
name = game.modules.get(id).data.title
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
name = game.system.data.title
|
||||||
|
}
|
||||||
|
provider.selectTitle = game.i18n.format(`drag-ruler.settings.speedProviderSettings.speedProvider.choices.${type}`, {name})
|
||||||
|
}
|
||||||
|
provider.isSelected = provider.id === selectedProvider
|
||||||
|
return provider
|
||||||
|
})
|
||||||
|
data.selectedProviderName = data.providers.find(provider => provider.isSelected).selectTitle
|
||||||
|
|
||||||
data.providerSelection = {
|
data.providerSelection = {
|
||||||
id: "speedProvider",
|
id: "speedProvider",
|
||||||
@@ -196,126 +197,122 @@ class SpeedProviderSettings extends FormApplication {
|
|||||||
hint: game.i18n.localize("drag-ruler.settings.speedProviderSettings.speedProvider.hint"),
|
hint: game.i18n.localize("drag-ruler.settings.speedProviderSettings.speedProvider.hint"),
|
||||||
type: String,
|
type: String,
|
||||||
choices: data.providers.reduce((choices, provider) => {
|
choices: data.providers.reduce((choices, provider) => {
|
||||||
choices[provider.id] = provider.selectTitle;
|
choices[provider.id] = provider.selectTitle
|
||||||
return choices;
|
return choices
|
||||||
}, {}),
|
}, {}),
|
||||||
value: selectedProvider,
|
value: selectedProvider,
|
||||||
isCheckbox: false,
|
isCheckbox: false,
|
||||||
isSelect: true,
|
isSelect: true,
|
||||||
isRange: false,
|
isRange: false,
|
||||||
};
|
}
|
||||||
return data;
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
async _updateObject(event, formData) {
|
async _updateObject(event, formData) {
|
||||||
const selectedSpeedProvider = game.user.isGM
|
const selectedSpeedProvider = game.user.isGM ? formData.speedProvider : game.settings.get(settingsKey, "speedProvider")
|
||||||
? formData.speedProvider
|
|
||||||
: game.settings.get(settingsKey, "speedProvider");
|
|
||||||
for (let [key, value] of Object.entries(formData)) {
|
for (let [key, value] of Object.entries(formData)) {
|
||||||
// Check if this is color, convert the value to an integer
|
// Check if this is color, convert the value to an integer
|
||||||
const splitKey = key.split(".", 3);
|
const splitKey = key.split(".", 3)
|
||||||
if (splitKey[0] !== "native") splitKey.shift();
|
if (splitKey[0] !== "native")
|
||||||
|
splitKey.shift()
|
||||||
if (splitKey.length >= 2 && splitKey[1] == "color") {
|
if (splitKey.length >= 2 && splitKey[1] == "color") {
|
||||||
value = parseInt(value.substring(1), 16);
|
value = parseInt(value.substring(1), 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't change settings for speed providers that aren't currently active
|
// Don't change settings for speed providers that aren't currently active
|
||||||
if (key !== "speedProvider" && !key.startsWith(selectedSpeedProvider)) continue;
|
if (key !== "speedProvider" && !key.startsWith(selectedSpeedProvider))
|
||||||
|
continue
|
||||||
|
|
||||||
// Get the key for the current setting
|
// Get the key for the current setting
|
||||||
let setting;
|
let setting
|
||||||
if (key === "speedProvider") setting = "speedProvider";
|
if (key === "speedProvider")
|
||||||
else setting = `speedProviders.${key}`;
|
setting = "speedProvider"
|
||||||
|
else
|
||||||
|
setting = `speedProviders.${key}`
|
||||||
|
|
||||||
// Get the old setting value
|
// Get the old setting value
|
||||||
const oldValue = game.settings.get(settingsKey, setting);
|
const oldValue = game.settings.get(settingsKey, setting)
|
||||||
|
|
||||||
// Only update the setting if it has been changed (this leaves the default in place if it hasn't been touched)
|
// Only update the setting if it has been changed (this leaves the default in place if it hasn't been touched)
|
||||||
if (value !== oldValue) game.settings.set(settingsKey, setting, value);
|
if (value !== oldValue)
|
||||||
|
game.settings.set(settingsKey, setting, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activate the configured speed provider
|
// Activate the configured speed provider
|
||||||
updateSpeedProvider();
|
updateSpeedProvider()
|
||||||
}
|
}
|
||||||
|
|
||||||
activateListeners(html) {
|
activateListeners(html) {
|
||||||
super.activateListeners(html);
|
super.activateListeners(html)
|
||||||
html.find("select[name=speedProvider]").change(this.onSpeedProviderChange.bind(this));
|
html.find("select[name=speedProvider]").change(this.onSpeedProviderChange.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
onSpeedProviderChange(event) {
|
onSpeedProviderChange(event) {
|
||||||
// Hide all module settings
|
// Hide all module settings
|
||||||
document
|
document.querySelectorAll(".drag-ruler-provider-settings").forEach(element => element.style.display = "none")
|
||||||
.querySelectorAll(".drag-ruler-provider-settings")
|
|
||||||
.forEach(element => (element.style.display = "none"));
|
|
||||||
// Show the settings block for the currently selected module
|
// Show the settings block for the currently selected module
|
||||||
document.getElementById(`drag-ruler.provider.${event.currentTarget.value}`).style.display = "";
|
document.getElementById(`drag-ruler.provider.${event.currentTarget.value}`).style.display = ""
|
||||||
|
|
||||||
// Recalculate window height
|
// Recalculate window height
|
||||||
this.element[0].style.height = null;
|
this.element[0].style.height = null
|
||||||
this.position.height = undefined;
|
this.position.height = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDomHex(value) {
|
function toDomHex(value) {
|
||||||
const hex = value.toString(16);
|
const hex = value.toString(16)
|
||||||
return "#" + "0".repeat(Math.max(0, 6 - hex.length)) + hex;
|
return "#" + "0".repeat(Math.max(0, 6 - hex.length)) + hex
|
||||||
}
|
}
|
||||||
|
|
||||||
function enumerateProviderSettings(provider) {
|
function enumerateProviderSettings(provider) {
|
||||||
const colorSettings = [];
|
const colorSettings = []
|
||||||
const unreachableColor = {
|
const unreachableColor = {id: "unreachable", name: "drag-ruler.settings.speedProviderSettings.color.unreachable.name"}
|
||||||
id: "unreachable",
|
|
||||||
name: "drag-ruler.settings.speedProviderSettings.color.unreachable.name",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Resolve settings for the colors
|
// Resolve settings for the colors
|
||||||
for (const color of provider.colors.concat([unreachableColor])) {
|
for (const color of provider.colors.concat([unreachableColor])) {
|
||||||
// Localize the name, if avaliable. If no name is available use the id as name
|
// Localize the name, if avaliable. If no name is available use the id as name
|
||||||
const colorName = color.name ? game.i18n.localize(color.name) : color.id;
|
const colorName = color.name ? game.i18n.localize(color.name) : color.id
|
||||||
let hint;
|
let hint
|
||||||
if (color === unreachableColor)
|
if (color === unreachableColor)
|
||||||
hint = game.i18n.localize("drag-ruler.settings.speedProviderSettings.color.unreachable.hint");
|
hint = game.i18n.localize("drag-ruler.settings.speedProviderSettings.color.unreachable.hint")
|
||||||
else
|
else
|
||||||
hint = game.i18n.format("drag-ruler.settings.speedProviderSettings.color.hint", {colorName});
|
hint = game.i18n.format("drag-ruler.settings.speedProviderSettings.color.hint", {colorName})
|
||||||
colorSettings.push({
|
colorSettings.push({
|
||||||
id: `${provider.id}.color.${color.id}`,
|
id: `${provider.id}.color.${color.id}`,
|
||||||
name: game.i18n.format("drag-ruler.settings.speedProviderSettings.color.name", {colorName}),
|
name: game.i18n.format("drag-ruler.settings.speedProviderSettings.color.name", {colorName}),
|
||||||
hint: hint,
|
hint: hint,
|
||||||
type: Number,
|
type: Number,
|
||||||
value: toDomHex(
|
value: toDomHex(game.settings.get(settingsKey, `speedProviders.${provider.id}.color.${color.id}`)),
|
||||||
game.settings.get(settingsKey, `speedProviders.${provider.id}.color.${color.id}`),
|
|
||||||
),
|
|
||||||
isCheckbox: false,
|
isCheckbox: false,
|
||||||
isSelect: false,
|
isSelect: false,
|
||||||
isRange: false,
|
isRange: false,
|
||||||
isColor: true,
|
isColor: true,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare regular settings
|
// Prepare regular settings
|
||||||
const settings = [];
|
const settings = []
|
||||||
for (const setting of provider.settings) {
|
for (const setting of provider.settings) {
|
||||||
try {
|
try {
|
||||||
if (setting.scope === "world" && !game.user.isGM) continue;
|
if (setting.scope === "world" && !game.user.isGM)
|
||||||
const s = duplicate(setting);
|
continue
|
||||||
s.id = `${provider.id}.setting.${s.id}`;
|
const s = duplicate(setting)
|
||||||
s.name = game.i18n.localize(s.name);
|
s.id = `${provider.id}.setting.${s.id}`
|
||||||
s.hint = game.i18n.localize(s.hint);
|
s.name = game.i18n.localize(s.name)
|
||||||
s.value = provider.getSetting(setting.id);
|
s.hint = game.i18n.localize(s.hint)
|
||||||
s.type = setting.type instanceof Function ? setting.type.name : "String";
|
s.value = provider.getSetting(setting.id)
|
||||||
s.isCheckbox = setting.type === Boolean;
|
s.type = setting.type instanceof Function ? setting.type.name : "String"
|
||||||
s.isSelect = s.choices !== undefined;
|
s.isCheckbox = setting.type === Boolean
|
||||||
s.isRange = setting.type === Number && s.range;
|
s.isSelect = s.choices !== undefined
|
||||||
s.isColor = false;
|
s.isRange = (setting.type === Number) && s.range
|
||||||
settings.push(s);
|
s.isColor = false
|
||||||
} catch (e) {
|
settings.push(s)
|
||||||
console.warn(
|
}
|
||||||
`Drag Ruler | The following error occured while rendering setting "${setting.id}" of module/system "${this.id}. It won't be displayed.`,
|
catch (e) {
|
||||||
);
|
console.warn(`Drag Ruler | The following error occured while rendering setting "${setting.id}" of module/system "${this.id}. It won't be displayed.`)
|
||||||
console.error(e);
|
console.error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return settings.concat(colorSettings);
|
return settings.concat(colorSettings)
|
||||||
}
|
}
|
||||||
@@ -11,13 +11,8 @@ Hooks.once("socketlib.ready", () => {
|
|||||||
export function updateCombatantDragRulerFlags(combat, updates) {
|
export function updateCombatantDragRulerFlags(combat, updates) {
|
||||||
const combatId = combat.id;
|
const combatId = combat.id;
|
||||||
// TODO Check if canvas.tokens.get is still neccessary in future foundry versions
|
// TODO Check if canvas.tokens.get is still neccessary in future foundry versions
|
||||||
return socket
|
return socket.executeAsGM(_socketUpdateCombatantDragRulerFlags, combatId, updates)
|
||||||
.executeAsGM(_socketUpdateCombatantDragRulerFlags, combatId, updates)
|
.then(() => currentSpeedProvider.onMovementHistoryUpdate(updates.map(update => canvas.tokens.get(combat.combatants.get(update._id).token.id))));
|
||||||
.then(() =>
|
|
||||||
currentSpeedProvider.onMovementHistoryUpdate(
|
|
||||||
updates.map(update => canvas.tokens.get(combat.combatants.get(update._id).token.id)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _socketUpdateCombatantDragRulerFlags(combatId, updates) {
|
async function _socketUpdateCombatantDragRulerFlags(combatId, updates) {
|
||||||
@@ -26,15 +21,12 @@ async function _socketUpdateCombatantDragRulerFlags(combatId, updates) {
|
|||||||
const requestedUpdates = updates.length;
|
const requestedUpdates = updates.length;
|
||||||
updates = updates.filter(update => {
|
updates = updates.filter(update => {
|
||||||
const actor = combat.combatants.get(update._id).actor;
|
const actor = combat.combatants.get(update._id).actor;
|
||||||
if (!actor) return false;
|
if (!actor)
|
||||||
|
return false;
|
||||||
return actor.testUserPermission(user, "OWNER");
|
return actor.testUserPermission(user, "OWNER");
|
||||||
});
|
});
|
||||||
if (updates.length !== requestedUpdates) {
|
if (updates.length !== requestedUpdates) {
|
||||||
console.warn(
|
console.warn(`Some of the movement history updates requested by user '${game.users.get(this.socketdata.userId).name}' were not performed because the user lacks owner permissions for those tokens`);
|
||||||
`Some of the movement history updates requested by user '${
|
|
||||||
game.users.get(this.socketdata.userId).name
|
|
||||||
}' were not performed because the user lacks owner permissions for those tokens`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
updates = updates.map(update => {
|
updates = updates.map(update => {
|
||||||
return {_id: update._id, flags: {dragRuler: update.dragRulerFlags}};
|
return {_id: update._id, flags: {dragRuler: update.dragRulerFlags}};
|
||||||
@@ -48,5 +40,6 @@ export function recalculate(tokens) {
|
|||||||
|
|
||||||
function _socketRecalculate(tokenIds) {
|
function _socketRecalculate(tokenIds) {
|
||||||
const ruler = canvas.controls.ruler;
|
const ruler = canvas.controls.ruler;
|
||||||
if (ruler.isDragRuler) ruler.dragRulerRecalculate(tokenIds);
|
if (ruler.isDragRuler)
|
||||||
|
ruler.dragRulerRecalculate(tokenIds);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {settingsKey} from "./settings.js";
|
import {settingsKey} from "./settings.js"
|
||||||
import {getDefaultDashMultiplier, getDefaultSpeedAttribute} from "./systems.js";
|
import {getDefaultDashMultiplier, getDefaultSpeedAttribute} from "./systems.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for all speed providers.
|
* Base class for all speed providers.
|
||||||
@@ -19,7 +19,7 @@ export class SpeedProvider {
|
|||||||
* Implementing this method is required for all speed providers
|
* Implementing this method is required for all speed providers
|
||||||
*/
|
*/
|
||||||
get colors() {
|
get colors() {
|
||||||
throw new Error("A SpeedProvider must implement the colors function");
|
throw new Error("A SpeedProvider must implement the colors function")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,7 +31,7 @@ export class SpeedProvider {
|
|||||||
* Implementing this method is required for all speed providers
|
* Implementing this method is required for all speed providers
|
||||||
*/
|
*/
|
||||||
getRanges(token) {
|
getRanges(token) {
|
||||||
throw new Error("A SpeedProvider must implement the getRanges function");
|
throw new Error("A SpeedProvider must implement the getRanges function")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,7 +44,7 @@ export class SpeedProvider {
|
|||||||
* Implementing this method is optional and only needs to be done if you want to provide custom provider settings
|
* Implementing this method is optional and only needs to be done if you want to provide custom provider settings
|
||||||
*/
|
*/
|
||||||
get settings() {
|
get settings() {
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,7 +53,28 @@ export class SpeedProvider {
|
|||||||
* Implementing this method is optional and only needs to be done if you want to provide a custom default for that color.
|
* Implementing this method is optional and only needs to be done if you want to provide a custom default for that color.
|
||||||
*/
|
*/
|
||||||
get defaultUnreachableColor() {
|
get defaultUnreachableColor() {
|
||||||
return 0xff0000;
|
return 0xFF0000
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the cost for a token to step into the specificed area.
|
||||||
|
* The area indicates the whole area that the token will occupy (for tokens larger than 1x1) the array will more than one entry.
|
||||||
|
* The return value should be an integer indicating a multiplicator by that the cost of that step should be increased.
|
||||||
|
* (1 is regular cost, 2 costs double, 3 costs triple, ...)
|
||||||
|
*
|
||||||
|
* Parameters:
|
||||||
|
* - options: An object used to configure Enhanced Terrain Layer's cost calculation. Ex: If options.ignoreGrid is set to true, then Euclidean measurement can be forced on a gridded map.
|
||||||
|
*
|
||||||
|
* This function is only called if the Enhanced Terrain Layer and Terrain Ruler modules are enabled.
|
||||||
|
*
|
||||||
|
* Implementing this method is optional and only needs to be done if you want to provide a custom cost function (for example to allow tokens to ignore difficult terrain)
|
||||||
|
*/
|
||||||
|
getCostForStep(token, area, options={}) {
|
||||||
|
// Lookup the cost for each square occupied by the token
|
||||||
|
options.token = token;
|
||||||
|
const costs = area.map(space => terrainRuler.getCost(space.x, space.y, options));
|
||||||
|
// Return the maximum of the costs
|
||||||
|
return costs.reduce((max, current) => Math.max(max, current))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,7 +86,7 @@ export class SpeedProvider {
|
|||||||
* Implementing this method is optional and only needs to be done if you want to disable Drag Ruler for some tokens.
|
* Implementing this method is optional and only needs to be done if you want to disable Drag Ruler for some tokens.
|
||||||
*/
|
*/
|
||||||
usesRuler(token) {
|
usesRuler(token) {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,14 +104,13 @@ export class SpeedProvider {
|
|||||||
*/
|
*/
|
||||||
getSetting(settingId) {
|
getSetting(settingId) {
|
||||||
try {
|
try {
|
||||||
return game.settings.get(settingsKey, `speedProviders.${this.id}.setting.${settingId}`);
|
return game.settings.get(settingsKey, `speedProviders.${this.id}.setting.${settingId}`)
|
||||||
} catch (e) {
|
}
|
||||||
|
catch (e) {
|
||||||
if (this.settings.some(setting => setting.id === settingId)) {
|
if (this.settings.some(setting => setting.id === settingId)) {
|
||||||
throw e;
|
throw e
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(`Drag Ruler | "${settingId}" is not a registered setting for "${this.id}". If you're the module/system developer, please add it to the return values of your Speed Providers "get settings()" function.`)
|
||||||
`Drag Ruler | "${settingId}" is not a registered setting for "${this.id}". If you're the module/system developer, please add it to the return values of your Speed Providers "get settings()" function.`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,34 +120,32 @@ export class SpeedProvider {
|
|||||||
* This function should neither be called or overridden by speed provider implementations
|
* This function should neither be called or overridden by speed provider implementations
|
||||||
*/
|
*/
|
||||||
constructor(id) {
|
constructor(id) {
|
||||||
this.id = id;
|
this.id = id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class GenericSpeedProvider extends SpeedProvider {
|
export class GenericSpeedProvider extends SpeedProvider {
|
||||||
get colors() {
|
get colors() {
|
||||||
return [
|
return [
|
||||||
{id: "walk", default: 0x00ff00, name: "drag-ruler.genericSpeedProvider.speeds.walk"},
|
{id: "walk", default: 0x00FF00, name: "drag-ruler.genericSpeedProvider.speeds.walk"},
|
||||||
{id: "dash", default: 0xffff00, name: "drag-ruler.genericSpeedProvider.speeds.dash"},
|
{id: "dash", default: 0xFFFF00, name: "drag-ruler.genericSpeedProvider.speeds.dash"}
|
||||||
];
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
getRanges(token) {
|
getRanges(token) {
|
||||||
const speedAttribute = this.getSetting("speedAttribute");
|
const speedAttribute = this.getSetting("speedAttribute")
|
||||||
if (!speedAttribute) return [];
|
if (!speedAttribute)
|
||||||
|
return []
|
||||||
const tokenSpeed = parseFloat(getProperty(token, speedAttribute));
|
const tokenSpeed = parseFloat(getProperty(token, speedAttribute));
|
||||||
if (tokenSpeed === undefined) {
|
if (tokenSpeed === undefined) {
|
||||||
console.warn(
|
console.warn(`Drag Ruler (Generic Speed Provider) | The configured token speed attribute "${speedAttribute}" didn't return a speed value. To use colors based on drag distance set the setting to the correct value (or clear the box to disable this feature).`)
|
||||||
`Drag Ruler (Generic Speed Provider) | The configured token speed attribute "${speedAttribute}" didn't return a speed value. To use colors based on drag distance set the setting to the correct value (or clear the box to disable this feature).`,
|
return []
|
||||||
);
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
const dashMultiplier = this.getSetting("dashMultiplier");
|
const dashMultiplier = this.getSetting("dashMultiplier")
|
||||||
if (!dashMultiplier) return [{range: tokenSpeed, color: "walk"}];
|
if (!dashMultiplier)
|
||||||
return [
|
return [{range: tokenSpeed, color: "walk"}]
|
||||||
{range: tokenSpeed, color: "walk"},
|
return [{range: tokenSpeed, color: "walk"}, {range: tokenSpeed * dashMultiplier, color: "dash"}]
|
||||||
{range: tokenSpeed * dashMultiplier, color: "dash"},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get settings() {
|
get settings() {
|
||||||
@@ -149,7 +167,7 @@ export class GenericSpeedProvider extends SpeedProvider {
|
|||||||
config: true,
|
config: true,
|
||||||
type: Number,
|
type: Number,
|
||||||
default: getDefaultDashMultiplier(),
|
default: getDefaultDashMultiplier(),
|
||||||
},
|
}
|
||||||
];
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
export function getDefaultSpeedAttribute() {
|
||||||
|
switch (game.system.id) {
|
||||||
|
case "CoC7":
|
||||||
|
return "actor.data.data.attribs.mov.value";
|
||||||
|
case "dcc":
|
||||||
|
return "actor.data.data.attributes.speed.value";
|
||||||
|
case "dnd4e":
|
||||||
|
return "actor.data.data.movement.walk.value";
|
||||||
|
case "dnd5e":
|
||||||
|
return "actor.data.data.attributes.movement.walk";
|
||||||
|
case "lancer":
|
||||||
|
return "actor.data.data.derived.speed";
|
||||||
|
case "pf1":
|
||||||
|
case "D35E":
|
||||||
|
return "actor.data.data.attributes.speed.land.total";
|
||||||
|
case "sfrpg":
|
||||||
|
return "actor.data.data.attributes.speed.value";
|
||||||
|
case "shadowrun5e":
|
||||||
|
return "actor.data.data.movement.walk.value";
|
||||||
|
case "swade":
|
||||||
|
return "actor.data.data.stats.speed.adjusted";
|
||||||
|
case "ds4":
|
||||||
|
return "actor.data.data.combatValues.movement.total";
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDefaultDashMultiplier() {
|
||||||
|
switch (game.system.id) {
|
||||||
|
case "swade":
|
||||||
|
return 0
|
||||||
|
case "dcc":
|
||||||
|
case "dnd4e":
|
||||||
|
case "dnd5e":
|
||||||
|
case "lancer":
|
||||||
|
case "pf1":
|
||||||
|
case "D35E":
|
||||||
|
case "sfrpg":
|
||||||
|
case "shadowrun5e":
|
||||||
|
case "ds4":
|
||||||
|
return 2;
|
||||||
|
case "CoC7":
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+80
-86
@@ -1,11 +1,10 @@
|
|||||||
import {getPixelsFromGridPosition} from "./foundry_fixes.js";
|
import {getPixelsFromGridPosition} from "./foundry_fixes.js"
|
||||||
import {findVertexSnapPoint} from "./hex_support.js";
|
import {findVertexSnapPoint} from "./hex_support.js";
|
||||||
import {disableSnap, moveWithoutAnimation, togglePathfinding} from "./keybindings.js";
|
import {disableSnap} from "./keybindings.js";
|
||||||
import {settingsKey} from "./settings.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++) {
|
||||||
yield [it1[i], it2[i]];
|
yield [it1[i], it2[i]]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,7 +17,7 @@ export function* enumeratedZip(it1, it2) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function* iterPairs(l) {
|
export function* iterPairs(l) {
|
||||||
for (let i = 1; i < l.length; i++) {
|
for (let i = 1;i < l.length;i++) {
|
||||||
yield [l[i - 1], l[i]];
|
yield [l[i - 1], l[i]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,15 +28,14 @@ export function sum(arr) {
|
|||||||
|
|
||||||
export function buildSnapPointTokenData(token) {
|
export function buildSnapPointTokenData(token) {
|
||||||
const tokenData = {
|
const tokenData = {
|
||||||
width: token.document.width,
|
width: token.data.width,
|
||||||
height: token.document.height,
|
height: token.data.height
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isModuleActive("hex-size-support")) {
|
if (isModuleActive("hex-size-support")) {
|
||||||
tokenData.hexSizeSupport = {};
|
tokenData.hexSizeSupport = {};
|
||||||
tokenData.hexSizeSupport.altSnappingFlag = CONFIG.hexSizeSupport.getAltSnappingFlag(token);
|
tokenData.hexSizeSupport.altSnappingFlag = CONFIG.hexSizeSupport.getAltSnappingFlag(token);
|
||||||
tokenData.hexSizeSupport.altOrientationFlag =
|
tokenData.hexSizeSupport.altOrientationFlag = CONFIG.hexSizeSupport.getAltOrientationFlag(token);
|
||||||
CONFIG.hexSizeSupport.getAltOrientationFlag(token);
|
|
||||||
tokenData.hexSizeSupport.borderSize = token.document.getFlag("hex-size-support", "borderSize");
|
tokenData.hexSizeSupport.borderSize = token.document.getFlag("hex-size-support", "borderSize");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +50,6 @@ export function getSnapPointForTokenDataObj(pos, tokenData) {
|
|||||||
return getSnapPointForTokenData(pos.x, pos.y, tokenData);
|
return getSnapPointForTokenData(pos.x, pos.y, tokenData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A copy of this function lives in the librouting module
|
|
||||||
function getSnapPointForTokenData(x, 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);
|
||||||
@@ -61,21 +58,27 @@ function getSnapPointForTokenData(x, y, tokenData) {
|
|||||||
if (tokenData.hexSizeSupport?.altSnappingFlag) {
|
if (tokenData.hexSizeSupport?.altSnappingFlag) {
|
||||||
if (tokenData.hexSizeSupport.borderSize % 2 === 0) {
|
if (tokenData.hexSizeSupport.borderSize % 2 === 0) {
|
||||||
const snapPoint = findVertexSnapPoint(x, y, tokenData.hexSizeSupport.altOrientationFlag);
|
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 {
|
|
||||||
return new PIXI.Point(...canvas.grid.getCenter(x, y));
|
|
||||||
}
|
}
|
||||||
} else {
|
else {
|
||||||
|
return new PIXI.Point(...canvas.grid.getCenter(x, y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
return new PIXI.Point(...canvas.grid.getCenter(x, y));
|
return new PIXI.Point(...canvas.grid.getCenter(x, y));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [topLeftX, topLeftY] = canvas.grid.getTopLeft(x, y);
|
const [topLeftX, topLeftY] = canvas.grid.getTopLeft(x, y);
|
||||||
let cellX, cellY;
|
let cellX, cellY;
|
||||||
if (tokenData.width % 2 === 0) cellX = x - canvas.grid.h / 2;
|
if (tokenData.width % 2 === 0)
|
||||||
else cellX = x;
|
cellX = x - canvas.grid.h / 2;
|
||||||
if (tokenData.height % 2 === 0) cellY = y - canvas.grid.h / 2;
|
else
|
||||||
else cellY = y;
|
cellX = x;
|
||||||
|
if (tokenData.height % 2 === 0)
|
||||||
|
cellY = y - canvas.grid.h / 2;
|
||||||
|
else
|
||||||
|
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
|
||||||
@@ -98,9 +101,11 @@ function getSnapPointForTokenData(x, y, tokenData) {
|
|||||||
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(tokenData.height) % 2 === 1 || tokenData.height < 1) {
|
}
|
||||||
|
else if (Math.round(tokenData.height) % 2 === 1 || tokenData.height < 1) {
|
||||||
snapY = centerY;
|
snapY = centerY;
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
snapY = centerY + canvas.grid.h / 2;
|
snapY = centerY + canvas.grid.h / 2;
|
||||||
}
|
}
|
||||||
return new PIXI.Point(snapX, snapY);
|
return new PIXI.Point(snapX, snapY);
|
||||||
@@ -119,13 +124,16 @@ export function getSnapPointForMeasuredTemplate(x, y) {
|
|||||||
|
|
||||||
export function getSnapPointForEntity(x, y, entity) {
|
export function getSnapPointForEntity(x, y, entity) {
|
||||||
const isToken = entity instanceof Token;
|
const isToken = entity instanceof Token;
|
||||||
if (isToken) return getSnapPointForToken(x, y, entity);
|
if (isToken)
|
||||||
else return getSnapPointForMeasuredTemplate(x, y);
|
return getSnapPointForToken(x, y, entity);
|
||||||
|
else
|
||||||
|
return getSnapPointForMeasuredTemplate(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function highlightTokenShape(position, shape, color, alpha) {
|
export function highlightTokenShape(position, shape, color, alpha) {
|
||||||
const layer = canvas.grid.highlightLayers[this.name];
|
const layer = canvas.grid.highlightLayers[this.name];
|
||||||
if (!layer) return false;
|
if ( !layer )
|
||||||
|
return false;
|
||||||
const area = getAreaFromPositionAndShape(position, shape);
|
const area = getAreaFromPositionAndShape(position, shape);
|
||||||
for (const space of area) {
|
for (const space of area) {
|
||||||
const [x, y] = getPixelsFromGridPosition(space.x, space.y);
|
const [x, y] = getPixelsFromGridPosition(space.x, space.y);
|
||||||
@@ -139,19 +147,22 @@ export function getAreaFromPositionAndShape(position, shape) {
|
|||||||
let y = position.y + space.y;
|
let y = position.y + space.y;
|
||||||
if (canvas.grid.isHex) {
|
if (canvas.grid.isHex) {
|
||||||
let shiftedRow;
|
let shiftedRow;
|
||||||
if (canvas.grid.grid.options.even) shiftedRow = 1;
|
if (canvas.grid.grid.options.even)
|
||||||
else shiftedRow = 0;
|
shiftedRow = 1
|
||||||
|
else
|
||||||
|
shiftedRow = 0
|
||||||
if (canvas.grid.grid.options.columns) {
|
if (canvas.grid.grid.options.columns) {
|
||||||
if (space.x % 2 !== 0 && position.x % 2 !== shiftedRow) {
|
if (space.x % 2 !== 0 && position.x % 2 !== shiftedRow) {
|
||||||
y += 1;
|
y += 1;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
if (space.y % 2 !== 0 && position.y % 2 !== shiftedRow) {
|
if (space.y % 2 !== 0 && position.y % 2 !== shiftedRow) {
|
||||||
x += 1;
|
x += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {x, y};
|
return {x, y}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,53 +170,40 @@ export function getTokenShape(token) {
|
|||||||
return getTokenShapeForTokenData(buildSnapPointTokenData(token), token.scene);
|
return getTokenShapeForTokenData(buildSnapPointTokenData(token), token.scene);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTokenShapeForTokenData(tokenData, scene = canvas.scene) {
|
export function getTokenShapeForTokenData(tokenData, scene=canvas.scene) {
|
||||||
if (scene.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
if (scene.data.gridType === CONST.GRID_TYPES.GRIDLESS) {
|
||||||
return [{x: 0, y: 0}];
|
return [{x: 0, y: 0}]
|
||||||
} else if (scene.grid.type === CONST.GRID_TYPES.SQUARE) {
|
}
|
||||||
const topOffset = -Math.floor(tokenData.height / 2);
|
else if (scene.data.gridType === CONST.GRID_TYPES.SQUARE) {
|
||||||
const leftOffset = -Math.floor(tokenData.width / 2);
|
const topOffset = -Math.floor(tokenData.height / 2)
|
||||||
const shape = [];
|
const leftOffset = -Math.floor(tokenData.width / 2)
|
||||||
for (let y = 0; y < tokenData.height; y++) {
|
const shape = []
|
||||||
for (let x = 0; x < tokenData.width; x++) {
|
for (let y = 0;y < tokenData.height;y++) {
|
||||||
shape.push({x: x + leftOffset, y: y + topOffset});
|
for (let x = 0;x < tokenData.width;x++) {
|
||||||
|
shape.push({x: x + leftOffset, y: y + topOffset})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return shape;
|
return shape
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
// Hex grids
|
// Hex grids
|
||||||
if (game.modules.get("hex-size-support")?.active && tokenData.hexSizeSupport.altSnappingFlag) {
|
if (game.modules.get("hex-size-support")?.active && tokenData.hexSizeSupport.altSnappingFlag) {
|
||||||
const borderSize = tokenData.hexSizeSupport.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([
|
shape = shape.concat([{x: 0, y: -1}, {x: -1, y: -1}]);
|
||||||
{x: 0, y: -1},
|
|
||||||
{x: -1, y: -1},
|
|
||||||
]);
|
|
||||||
if (borderSize >= 3)
|
if (borderSize >= 3)
|
||||||
shape = shape.concat([
|
shape = shape.concat([{x: 0, y: 1}, {x: -1, y: 1}, {x: -1, y: 0}, {x: 1, y: 0}]);
|
||||||
{x: 0, y: 1},
|
|
||||||
{x: -1, y: 1},
|
|
||||||
{x: -1, y: 0},
|
|
||||||
{x: 1, y: 0},
|
|
||||||
]);
|
|
||||||
if (borderSize >= 4)
|
if (borderSize >= 4)
|
||||||
shape = shape.concat([
|
shape = shape.concat([{x: -2, y: -1}, {x: 1, y: -1}, {x: -1, y: -2}, {x: 0, y: -2}, {x: 1, y: -2}])
|
||||||
{x: -2, y: -1},
|
|
||||||
{x: 1, y: -1},
|
|
||||||
{x: -1, y: -2},
|
|
||||||
{x: 0, y: -2},
|
|
||||||
{x: 1, y: -2},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (Boolean(tokenData.hexSizeSupport.altOrientationFlag) !== 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 => {
|
shape = shape.map(space => {return {x: space.y, y: space.x}});
|
||||||
return {x: space.y, y: space.x};
|
|
||||||
});
|
|
||||||
return shape;
|
return shape;
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
return [{x: 0, y: 0}];
|
return [{x: 0, y: 0}];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,12 +211,13 @@ export function getTokenShapeForTokenData(tokenData, scene = canvas.scene) {
|
|||||||
|
|
||||||
export function getTokenSize(token) {
|
export function getTokenSize(token) {
|
||||||
let w, h;
|
let w, h;
|
||||||
const hexSizeSupportBorderSize = token.document.flags["hex-size-support"]?.borderSize;
|
const hexSizeSupportBorderSize = token.data.flags["hex-size-support"]?.borderSize;
|
||||||
if (hexSizeSupportBorderSize > 0) {
|
if (hexSizeSupportBorderSize > 0) {
|
||||||
w = h = hexSizeSupportBorderSize;
|
w = h = hexSizeSupportBorderSize
|
||||||
} else {
|
}
|
||||||
w = token.document.width;
|
else {
|
||||||
h = token.document.height;
|
w = token.data.width
|
||||||
|
h = token.data.height
|
||||||
}
|
}
|
||||||
return {w, h};
|
return {w, h};
|
||||||
}
|
}
|
||||||
@@ -227,7 +226,7 @@ export function getTokenSize(token) {
|
|||||||
// This function applies an offset to to the waypoints that will move the ruler from the edge to the center of the cell
|
// This function applies an offset to to the waypoints that will move the ruler from the edge to the center of the cell
|
||||||
export function applyTokenSizeOffset(waypoints, token) {
|
export function applyTokenSizeOffset(waypoints, token) {
|
||||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||||
return waypoints;
|
return waypoints
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenSize = getTokenSize(token);
|
const tokenSize = getTokenSize(token);
|
||||||
@@ -238,17 +237,21 @@ export function applyTokenSizeOffset(waypoints, token) {
|
|||||||
if (canvas.grid.grid.options.columns) {
|
if (canvas.grid.grid.options.columns) {
|
||||||
if (tokenSize.w % 2 === 0) {
|
if (tokenSize.w % 2 === 0) {
|
||||||
waypointOffset.x = canvas.grid.w / 2;
|
waypointOffset.x = canvas.grid.w / 2;
|
||||||
if (!isAltOrientation) waypointOffset.x *= -1;
|
if (!isAltOrientation)
|
||||||
|
waypointOffset.x *= -1;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
if (tokenSize.h % 2 === 0) {
|
if (tokenSize.h % 2 === 0) {
|
||||||
waypointOffset.y = canvas.grid.h / 2;
|
waypointOffset.y = canvas.grid.h / 2;
|
||||||
if (isAltOrientation) waypointOffset.y *= -1;
|
if (isAltOrientation)
|
||||||
|
waypointOffset.y *= -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If hex size support isn't active leave the waypoints like they are
|
// If hex size support isn't active leave the waypoints like they are
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
if (tokenSize.w % 2 === 0) {
|
if (tokenSize.w % 2 === 0) {
|
||||||
waypointOffset.x = canvas.grid.w / 2;
|
waypointOffset.x = canvas.grid.w / 2;
|
||||||
}
|
}
|
||||||
@@ -257,7 +260,7 @@ export function applyTokenSizeOffset(waypoints, token) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return waypoints.map(w => new PIXI.Point(w.x + waypointOffset.x, w.y + waypointOffset.y));
|
return waypoints.map(w => new PIXI.Point(w.x + waypointOffset.x, w.y + waypointOffset.y))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setSnapParameterOnOptions(sourceObject, options) {
|
export function setSnapParameterOnOptions(sourceObject, options) {
|
||||||
@@ -266,7 +269,8 @@ export function setSnapParameterOnOptions(sourceObject, options) {
|
|||||||
options.snapOverrideActive = true;
|
options.snapOverrideActive = true;
|
||||||
options.snap = sourceObject.snapOverride.snap;
|
options.snap = sourceObject.snapOverride.snap;
|
||||||
sourceObject.snapOverride = undefined; // remove it to prevent any lingering data issues
|
sourceObject.snapOverride = undefined; // remove it to prevent any lingering data issues
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
options.snap = !disableSnap;
|
options.snap = !disableSnap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,9 +280,7 @@ export function isClose(a, b, delta) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getMeasurePosition() {
|
export function getMeasurePosition() {
|
||||||
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(
|
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens);
|
||||||
canvas.tokens,
|
|
||||||
);
|
|
||||||
const rulerOffset = canvas.controls.ruler.rulerOffset;
|
const rulerOffset = canvas.controls.ruler.rulerOffset;
|
||||||
const measurePosition = {x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y};
|
const measurePosition = {x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y};
|
||||||
return measurePosition;
|
return measurePosition;
|
||||||
@@ -292,13 +294,5 @@ export function early_isGM() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isModuleActive(moduleName) {
|
export function isModuleActive(moduleName) {
|
||||||
return game.modules.get(moduleName)?.active;
|
return game.modules.get(moduleName)?.active
|
||||||
}
|
|
||||||
|
|
||||||
export function isPathfindingEnabled() {
|
|
||||||
if (!window.routinglib) return false;
|
|
||||||
if (this.user !== game.user) return false;
|
|
||||||
if (!game.user.isGM && !game.settings.get(settingsKey, "allowPathfinding")) return false;
|
|
||||||
if (moveWithoutAnimation) return false;
|
|
||||||
return game.settings.get(settingsKey, "autoPathfinding") != togglePathfinding;
|
|
||||||
}
|
}
|
||||||
+14
-19
@@ -1,12 +1,10 @@
|
|||||||
{
|
{
|
||||||
"id": "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.13.0",
|
"version": "1.12.8",
|
||||||
"compatibility": {
|
"minimumCoreVersion" : "9.245",
|
||||||
"minimum": "10",
|
"compatibleCoreVersion" : "9",
|
||||||
"verified": "10"
|
|
||||||
},
|
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "Manuel Vögele",
|
"name": "Manuel Vögele",
|
||||||
@@ -15,9 +13,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"esmodules": [
|
"esmodules": [
|
||||||
"src/libwrapper_shim.js",
|
"js/libwrapper_shim.js",
|
||||||
"src/main.js",
|
"js/main.js",
|
||||||
"src/socket.js"
|
"js/socket.js"
|
||||||
],
|
],
|
||||||
"templates": [
|
"templates": [
|
||||||
"speed_provider_settings.html"
|
"speed_provider_settings.html"
|
||||||
@@ -59,18 +57,15 @@
|
|||||||
"path": "lang/zh-tw.json"
|
"path": "lang/zh-tw.json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"relationships": {
|
"dependencies": [
|
||||||
"requires": [
|
{
|
||||||
{
|
"name": "socketlib",
|
||||||
"id": "socketlib",
|
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-socketlib/master/module.json"
|
||||||
"type": "module",
|
}
|
||||||
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-socketlib/master/module.json"
|
],
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"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/archive/v1.13.0.zip",
|
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/releases/download/v1.12.8/drag-ruler-1.12.8.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",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
target/
|
||||||
Generated
+232
@@ -0,0 +1,232 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
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]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "console_error_panic_hook"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"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]]
|
||||||
|
name = "gridless-pathfinding"
|
||||||
|
version = "1.12.8"
|
||||||
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
|
"js-sys",
|
||||||
|
"rustc-hash",
|
||||||
|
"sha1",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.56"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04"
|
||||||
|
dependencies = [
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.125"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-xid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
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]]
|
||||||
|
name = "syn"
|
||||||
|
version = "1.0.86"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-xid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.79"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-backend"
|
||||||
|
version = "0.2.79"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.79"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.79"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-backend",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.79"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
[package]
|
||||||
|
name = "gridless-pathfinding"
|
||||||
|
version = "1.12.8"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
#debug = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
console_error_panic_hook = "0.1.7"
|
||||||
|
js-sys = "0.3.56"
|
||||||
|
rustc-hash = "1.1.0"
|
||||||
|
sha1 = "0.10.1"
|
||||||
|
wasm-bindgen = "0.2.79"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
hard_tabs = true
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
extern "C" {
|
||||||
|
pub type JsPoint;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter)]
|
||||||
|
fn x(this: &JsPoint) -> f64;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter)]
|
||||||
|
fn y(this: &JsPoint) -> f64;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct Point {
|
||||||
|
pub x: f64,
|
||||||
|
pub y: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Point {
|
||||||
|
pub fn new(x: f64, y: f64) -> Self {
|
||||||
|
Self { x, y }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_line_x(line: &Line, x: f64) -> Self {
|
||||||
|
let y = line.calc_y(x);
|
||||||
|
Self { x, y }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn distance_to(&self, to: Point) -> f64 {
|
||||||
|
(self.y - to.y).hypot(self.x - to.x)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_same_as(&self, other: &Self) -> bool {
|
||||||
|
let e = 0.000001;
|
||||||
|
(self.x - other.x).abs() < e && (self.y - other.y).abs() < e
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_in_rectangle(&self, rect: &Rectangle) -> bool {
|
||||||
|
between(self.x, rect.left.p1.x, rect.right.p1.x)
|
||||||
|
&& between(self.y, rect.top.p1.y, rect.bottom.p1.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for Point {}
|
||||||
|
|
||||||
|
impl PartialEq for Point {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.x == other.x && self.y == other.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for Point {
|
||||||
|
fn hash<H: Hasher>(&self, hasher: &mut H) {
|
||||||
|
self.x.to_bits().hash(hasher);
|
||||||
|
self.y.to_bits().hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&JsPoint> for Point {
|
||||||
|
fn from(point: &JsPoint) -> Self {
|
||||||
|
Self::new(point.x(), point.y())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct Line {
|
||||||
|
pub m: f64,
|
||||||
|
pub b: f64,
|
||||||
|
pub p1: Point,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Line {
|
||||||
|
pub fn new(m: f64, b: f64, p1: Point) -> Self {
|
||||||
|
Self { m, b, p1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_points(p1: Point, p2: Point) -> Self {
|
||||||
|
let m = (p1.y - p2.y) / (p1.x - p2.x);
|
||||||
|
let b = p1.y - m * p1.x;
|
||||||
|
Self { m, b, p1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_point_and_angle(p1: Point, angle: f64) -> Self {
|
||||||
|
let p2 = Point {
|
||||||
|
x: p1.x - angle.cos(),
|
||||||
|
y: p1.y - angle.sin(),
|
||||||
|
};
|
||||||
|
Line::from_points(p1, p2)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_vertical(&self) -> bool {
|
||||||
|
self.m.is_infinite()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_horizontal(&self) -> bool {
|
||||||
|
self.m == 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calc_x(&self, y: f64) -> f64 {
|
||||||
|
(y - self.b) / self.m
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calc_y(&self, x: f64) -> f64 {
|
||||||
|
self.m * x + self.b
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn intersection(&self, other: &Line) -> Option<Point> {
|
||||||
|
// Are both lines vertical?
|
||||||
|
if self.is_vertical() && other.is_vertical() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Are the lines paralell?
|
||||||
|
if (self.m - other.m).abs() < 0.00000005 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is one of the lines vertical?
|
||||||
|
if self.is_vertical() || other.is_vertical() {
|
||||||
|
let vertical;
|
||||||
|
let regular;
|
||||||
|
if self.is_vertical() {
|
||||||
|
vertical = self;
|
||||||
|
regular = other;
|
||||||
|
} else {
|
||||||
|
vertical = other;
|
||||||
|
regular = self;
|
||||||
|
}
|
||||||
|
return Some(Point::from_line_x(regular, vertical.p1.x));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate x coordinate of intersection point between both lines
|
||||||
|
// Find intersection point: x * m1 + b1 = x * m2 + b2
|
||||||
|
// Solve for x: x = (b1 - b2) / (m2 - m1)
|
||||||
|
let x = (self.b - other.b) / (other.m - self.m);
|
||||||
|
if self.m.abs() < other.m.abs() {
|
||||||
|
Some(Point::from_line_x(self, x))
|
||||||
|
} else {
|
||||||
|
Some(Point::from_line_x(other, x))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_perpendicular_through_point(&self, p: Point) -> Self {
|
||||||
|
let m = -1.0 / self.m;
|
||||||
|
let b = p.y - m * p.x;
|
||||||
|
Self { m, b, p1: p }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct LineSegment {
|
||||||
|
pub p1: Point,
|
||||||
|
pub p2: Point,
|
||||||
|
pub line: Line,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LineSegment {
|
||||||
|
pub fn new(p1: Point, p2: Point) -> Self {
|
||||||
|
Self {
|
||||||
|
p1,
|
||||||
|
p2,
|
||||||
|
line: Line::from_points(p1, p2),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn intersection(&self, other: &LineSegment) -> Option<Point> {
|
||||||
|
let intersection = self.line.intersection(&other.line);
|
||||||
|
intersection.filter(|intersection| {
|
||||||
|
self.is_intersection_on_segment(*intersection)
|
||||||
|
&& other.is_intersection_on_segment(*intersection)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_intersection_on_segment(&self, intersection: Point) -> bool {
|
||||||
|
if intersection.is_same_as(&self.p1) || intersection.is_same_as(&self.p2) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if self.line.is_vertical() || self.line.m.abs() > 1.0 {
|
||||||
|
return between(intersection.y, self.p1.y, self.p2.y);
|
||||||
|
}
|
||||||
|
between(intersection.x, self.p1.x, self.p2.x)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn intersects_rect(&self, rect: &Rectangle) -> bool {
|
||||||
|
if self.p1.is_in_rectangle(rect) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
[rect.left, rect.top, rect.right, rect.bottom]
|
||||||
|
.iter()
|
||||||
|
.any(|edge| self.intersection(edge).is_some())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn between<T: Copy + PartialOrd>(num: T, a: T, b: T) -> bool {
|
||||||
|
let (min, max) = if a < b { (a, b) } else { (b, a) };
|
||||||
|
num >= min && num <= max
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
extern "C" {
|
||||||
|
pub type JsRectangle;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter)]
|
||||||
|
fn left(this: &JsRectangle) -> f64;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter)]
|
||||||
|
fn top(this: &JsRectangle) -> f64;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter)]
|
||||||
|
fn right(this: &JsRectangle) -> f64;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter)]
|
||||||
|
fn bottom(this: &JsRectangle) -> f64;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct Rectangle {
|
||||||
|
pub left: LineSegment,
|
||||||
|
pub top: LineSegment,
|
||||||
|
pub right: LineSegment,
|
||||||
|
pub bottom: LineSegment,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rectangle {
|
||||||
|
pub fn new(left: f64, top: f64, right: f64, bottom: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
left: LineSegment::new(Point::new(left, top), Point::new(left, bottom)),
|
||||||
|
top: LineSegment::new(Point::new(left, top), Point::new(right, top)),
|
||||||
|
right: LineSegment::new(Point::new(right, top), Point::new(right, bottom)),
|
||||||
|
bottom: LineSegment::new(Point::new(left, bottom), Point::new(right, bottom)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&JsRectangle> for Rectangle {
|
||||||
|
fn from(rect: &JsRectangle) -> Self {
|
||||||
|
let left = rect.left();
|
||||||
|
let top = rect.top();
|
||||||
|
let right = rect.right();
|
||||||
|
let bottom = rect.bottom();
|
||||||
|
Self::new(left, top, right, bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct Circle {
|
||||||
|
pub center: Point,
|
||||||
|
pub radius: f64,
|
||||||
|
}
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
use js_sys::Array;
|
||||||
|
use sha1::{Digest, Sha1};
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
geometry::{Circle, LineSegment, Point, Rectangle},
|
||||||
|
pathfinder::{DiscoveredNodePtr, Pathfinder},
|
||||||
|
util::Windows,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
macro_rules! log {
|
||||||
|
( $( $t:tt )* ) => {
|
||||||
|
log(&format!( $( $t )* ));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
extern "C" {
|
||||||
|
#[wasm_bindgen(js_namespace = console, js_name=warn)]
|
||||||
|
pub fn log(s: &str);
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_namespace = ["dragRuler", "private"], js_name=terrainRulerWrapper)]
|
||||||
|
pub fn distance_with_terrain(a: Point, b: Point) -> f64;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
extern "C" {
|
||||||
|
pub type JsWall;
|
||||||
|
pub type JsWallData;
|
||||||
|
pub type JsWallFlags;
|
||||||
|
pub type JsWallHeight;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter)]
|
||||||
|
fn data(this: &JsWall) -> JsWallData;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter)]
|
||||||
|
fn c(this: &JsWallData) -> Vec<f64>;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter, js_name = "door")]
|
||||||
|
fn door_type(this: &JsWallData) -> DoorType;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter, js_name = "ds")]
|
||||||
|
fn door_state(this: &JsWallData) -> DoorState;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter, js_name = "move")]
|
||||||
|
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]
|
||||||
|
extern "C" {
|
||||||
|
pub type JsPoint;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter)]
|
||||||
|
fn x(this: &JsPoint) -> f64;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter)]
|
||||||
|
fn y(this: &JsPoint) -> f64;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<JsPoint> for Point {
|
||||||
|
fn from(point: JsPoint) -> Self {
|
||||||
|
Point {
|
||||||
|
x: point.x(),
|
||||||
|
y: point.y(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
extern "C" {
|
||||||
|
pub type JsTerrainInfo;
|
||||||
|
pub type JsTerrainObject;
|
||||||
|
pub type JsTerrainShape;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter)]
|
||||||
|
fn object(this: &JsTerrainInfo) -> JsTerrainObject;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter)]
|
||||||
|
fn shape(this: &JsTerrainInfo) -> JsTerrainShape;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter)]
|
||||||
|
fn x(this: &JsTerrainObject) -> f64;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter)]
|
||||||
|
fn y(this: &JsTerrainObject) -> f64;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter)]
|
||||||
|
fn width(this: &JsTerrainObject) -> f64;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter)]
|
||||||
|
fn height(this: &JsTerrainObject) -> f64;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter, js_name = "type")]
|
||||||
|
fn shape_type(this: &JsTerrainShape) -> u32;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter)]
|
||||||
|
fn x(this: &JsTerrainShape) -> f64;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter)]
|
||||||
|
fn y(this: &JsTerrainShape) -> f64;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter)]
|
||||||
|
fn radius(this: &JsTerrainShape) -> f64;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter)]
|
||||||
|
fn points(this: &JsTerrainShape) -> Vec<f64>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JsTerrainObject {
|
||||||
|
fn to_bounding_rect(&self) -> Rectangle {
|
||||||
|
let left = self.x();
|
||||||
|
let top = self.y();
|
||||||
|
let right = left + self.width();
|
||||||
|
let bottom = top + self.height();
|
||||||
|
Rectangle::new(left, top, right, bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JsTerrainShape {
|
||||||
|
fn to_segments(&self, x: f64, y: f64) -> Vec<LineSegment> {
|
||||||
|
let points = self.points();
|
||||||
|
assert!(points.len() % 2 == 0);
|
||||||
|
points
|
||||||
|
.chunks(2)
|
||||||
|
.map(|coordinates| Point::new(coordinates[0] + x, coordinates[1] + y))
|
||||||
|
.windows()
|
||||||
|
.map(|(p1, p2)| LineSegment::new(p1, p2))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_circle(&self, x: f64, y: f64) -> Circle {
|
||||||
|
let center = Point::new(self.x() + x, self.y() + y);
|
||||||
|
let radius = self.radius();
|
||||||
|
Circle { center, radius }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&JsTerrainInfo> for TerrainShape {
|
||||||
|
fn from(terrain: &JsTerrainInfo) -> Self {
|
||||||
|
let shape = terrain.shape();
|
||||||
|
let object = terrain.object();
|
||||||
|
let x = object.x();
|
||||||
|
let y = object.y();
|
||||||
|
match shape.shape_type() {
|
||||||
|
0 => TerrainShape::Polygon(shape.to_segments(x, y)),
|
||||||
|
2 => TerrainShape::Circle(shape.to_circle(x, y)),
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TerrainShape {
|
||||||
|
Polygon(Vec<LineSegment>),
|
||||||
|
Circle(Circle),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Terrain {
|
||||||
|
pub shape: TerrainShape,
|
||||||
|
pub bounding_box: Rectangle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&JsTerrainInfo> for Terrain {
|
||||||
|
fn from(terrain: &JsTerrainInfo) -> Self {
|
||||||
|
let bounding_box = terrain.object().to_bounding_rect();
|
||||||
|
let shape = terrain.into();
|
||||||
|
Self {
|
||||||
|
bounding_box,
|
||||||
|
shape,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
pub enum DoorState {
|
||||||
|
CLOSED = 0,
|
||||||
|
OPEN = 1,
|
||||||
|
LOCKED = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<usize> for DoorState {
|
||||||
|
type Error = ();
|
||||||
|
fn try_from(value: usize) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
x if x == Self::CLOSED as usize => Ok(Self::CLOSED),
|
||||||
|
x if x == Self::OPEN as usize => Ok(Self::OPEN),
|
||||||
|
x if x == Self::LOCKED as usize => Ok(Self::LOCKED),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
pub enum DoorType {
|
||||||
|
NONE = 0,
|
||||||
|
DOOR = 1,
|
||||||
|
SECRET = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<usize> for DoorType {
|
||||||
|
type Error = ();
|
||||||
|
fn try_from(value: usize) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
x if x == Self::NONE as usize => Ok(Self::NONE),
|
||||||
|
x if x == Self::DOOR as usize => Ok(Self::DOOR),
|
||||||
|
x if x == Self::SECRET as usize => Ok(Self::SECRET),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
pub enum WallSenseType {
|
||||||
|
NONE = 0,
|
||||||
|
LIMITED = 10,
|
||||||
|
NORMAL = 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<usize> for WallSenseType {
|
||||||
|
type Error = ();
|
||||||
|
fn try_from(value: usize) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
x if x == Self::NONE as usize => Ok(Self::NONE),
|
||||||
|
x if x == Self::LIMITED as usize => Ok(Self::LIMITED),
|
||||||
|
x if x == Self::NORMAL as usize => Ok(Self::NORMAL),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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)]
|
||||||
|
pub struct Wall {
|
||||||
|
pub p1: Point,
|
||||||
|
pub p2: Point,
|
||||||
|
pub door_type: DoorType,
|
||||||
|
pub door_state: DoorState,
|
||||||
|
pub move_type: WallSenseType,
|
||||||
|
pub height: WallHeight,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Wall {
|
||||||
|
pub fn new(
|
||||||
|
p1: Point,
|
||||||
|
p2: Point,
|
||||||
|
door_type: DoorType,
|
||||||
|
door_state: DoorState,
|
||||||
|
move_type: WallSenseType,
|
||||||
|
height: WallHeight,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
p1,
|
||||||
|
p2,
|
||||||
|
door_type,
|
||||||
|
door_state,
|
||||||
|
move_type,
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_door(&self) -> bool {
|
||||||
|
self.door_type != DoorType::NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_open(&self) -> bool {
|
||||||
|
self.door_state == DoorState::OPEN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Wall {
|
||||||
|
fn from_js(wall: &JsWall, enable_height: bool) -> Self {
|
||||||
|
let data = wall.data();
|
||||||
|
let mut c = data.c();
|
||||||
|
c.iter_mut().for_each(|val| *val = val.round());
|
||||||
|
let height = if enable_height {
|
||||||
|
data.flags().wall_height().into()
|
||||||
|
} else {
|
||||||
|
WallHeight::default()
|
||||||
|
};
|
||||||
|
Self::new(
|
||||||
|
Point::new(c[0], c[1]),
|
||||||
|
Point::new(c[2], c[3]),
|
||||||
|
data.door_type(),
|
||||||
|
data.door_state(),
|
||||||
|
data.move_type(),
|
||||||
|
height,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn initialize(
|
||||||
|
js_walls: Vec<JsValue>,
|
||||||
|
js_terrain: Vec<JsTerrainInfo>,
|
||||||
|
token_size: f64,
|
||||||
|
token_elevation: f64,
|
||||||
|
enable_height: bool,
|
||||||
|
) -> Pathfinder {
|
||||||
|
let mut walls = Vec::with_capacity(js_walls.len());
|
||||||
|
for wall in js_walls {
|
||||||
|
let wall = JsWall::from(wall);
|
||||||
|
walls.push(Wall::from_js(&wall, enable_height));
|
||||||
|
}
|
||||||
|
let terrain = js_terrain.iter().map(|terrain| terrain.into()).collect();
|
||||||
|
Pathfinder::initialize(walls, terrain, token_size, token_elevation)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn free(pathfinder: Pathfinder) {
|
||||||
|
drop(pathfinder);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[wasm_bindgen(js_name=findPath)]
|
||||||
|
pub fn find_path(pathfinder: &mut Pathfinder, from: JsPoint, to: JsPoint) -> Option<Array> {
|
||||||
|
pathfinder
|
||||||
|
.find_path(from.into(), to.into())
|
||||||
|
.map(|first_node| first_node.iter_path().map(JsValue::from).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[wasm_bindgen(js_name=debugGetPathfindingPoints)]
|
||||||
|
pub fn debug_get_pathfinding_points(pathfinder: &Pathfinder) -> Array {
|
||||||
|
pathfinder
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.map(|node| node.borrow().point)
|
||||||
|
.map(JsValue::from)
|
||||||
|
.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 {
|
||||||
|
fn iter_path(&self) -> PathIterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IteratePath for DiscoveredNodePtr {
|
||||||
|
fn iter_path(&self) -> PathIterator {
|
||||||
|
PathIterator {
|
||||||
|
current_node: Some(self.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PathIterator {
|
||||||
|
current_node: Option<DiscoveredNodePtr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for PathIterator {
|
||||||
|
type Item = Point;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if let Some(node) = self.current_node.clone() {
|
||||||
|
let point = node.borrow().node.borrow().point;
|
||||||
|
self.current_node = node.borrow().previous.clone();
|
||||||
|
Some(point)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
mod geometry;
|
||||||
|
#[macro_use]
|
||||||
|
mod js_api;
|
||||||
|
mod pathfinder;
|
||||||
|
mod ptr_indexed_hash_set;
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
#[wasm_bindgen(start)]
|
||||||
|
pub fn main() {
|
||||||
|
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
|
||||||
|
}
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
use std::{cell::RefCell, f64::consts::PI, rc::Rc};
|
||||||
|
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
geometry::{LineSegment, Point, Rectangle},
|
||||||
|
js_api::{distance_with_terrain, Terrain, TerrainShape, Wall, WallSenseType, log},
|
||||||
|
ptr_indexed_hash_set::PtrIndexedHashSet,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Edge {
|
||||||
|
target: NodePtr,
|
||||||
|
cost: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Node {
|
||||||
|
pub point: Point,
|
||||||
|
edges: Option<Vec<Edge>>,
|
||||||
|
final_edge: Option<Option<Edge>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node {
|
||||||
|
pub fn new(point: Point) -> Self {
|
||||||
|
Self {
|
||||||
|
point,
|
||||||
|
edges: None,
|
||||||
|
final_edge: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn iter_edges(
|
||||||
|
&self,
|
||||||
|
) -> std::iter::Chain<std::slice::Iter<'_, Edge>, std::option::Iter<'_, Edge>> {
|
||||||
|
self.edges
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.chain(self.final_edge.as_ref().unwrap().iter())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodePtr = Rc<RefCell<Node>>;
|
||||||
|
|
||||||
|
impl From<Node> for NodePtr {
|
||||||
|
fn from(node: Node) -> Self {
|
||||||
|
Rc::new(RefCell::new(node))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DiscoveredNode {
|
||||||
|
pub node: NodePtr,
|
||||||
|
cost: f64,
|
||||||
|
estimated: f64,
|
||||||
|
pub previous: Option<DiscoveredNodePtr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type DiscoveredNodePtr = Rc<RefCell<DiscoveredNode>>;
|
||||||
|
|
||||||
|
impl From<DiscoveredNode> for DiscoveredNodePtr {
|
||||||
|
fn from(node: DiscoveredNode) -> Self {
|
||||||
|
Rc::new(RefCell::new(node))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub struct NodeStorage {
|
||||||
|
regular_nodes: Vec<NodePtr>,
|
||||||
|
final_node: Option<NodePtr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type NodeStorageIterator<'a> = std::iter::Chain<
|
||||||
|
std::slice::Iter<'a, Rc<RefCell<Node>>>,
|
||||||
|
std::option::Iter<'a, Rc<RefCell<Node>>>,
|
||||||
|
>;
|
||||||
|
|
||||||
|
impl NodeStorage {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push(&mut self, node: NodePtr) {
|
||||||
|
self.regular_nodes.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initialize_edges(&mut self, node: &NodePtr, walls: &[LineSegment], terrain: &[Rectangle]) {
|
||||||
|
if node.borrow().final_edge.is_none() {
|
||||||
|
let final_edge = self
|
||||||
|
.final_node
|
||||||
|
.as_ref()
|
||||||
|
.filter(|neighbor| {
|
||||||
|
!self.collides_with_wall(
|
||||||
|
&LineSegment::new(node.borrow().point, neighbor.borrow().point),
|
||||||
|
walls,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(|neighbor| Edge {
|
||||||
|
target: neighbor.clone(),
|
||||||
|
cost: Self::measure_distance(
|
||||||
|
node.borrow().point,
|
||||||
|
neighbor.borrow().point,
|
||||||
|
terrain,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
node.borrow_mut().final_edge = Some(final_edge);
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.borrow().edges.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let point = node.borrow().point;
|
||||||
|
let mut edges = Vec::new();
|
||||||
|
for neighbor in &self.regular_nodes {
|
||||||
|
if Rc::ptr_eq(neighbor, node) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let neighbor_point = neighbor.borrow().point;
|
||||||
|
if !self.collides_with_wall(&LineSegment::new(point, neighbor_point), walls) {
|
||||||
|
let cost =
|
||||||
|
Self::measure_distance(node.borrow().point, neighbor.borrow().point, terrain);
|
||||||
|
edges.push(Edge {
|
||||||
|
target: neighbor.clone(),
|
||||||
|
cost,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node.borrow_mut().edges = Some(edges);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collides_with_wall(&self, line: &LineSegment, walls: &[LineSegment]) -> bool {
|
||||||
|
walls.iter().any(|wall| line.intersection(wall).is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure_distance(a: Point, b: Point, terrain: &[Rectangle]) -> f64 {
|
||||||
|
let segment = LineSegment::new(a, b);
|
||||||
|
if terrain.iter().any(|rect| segment.intersects_rect(rect)) {
|
||||||
|
distance_with_terrain(a, b)
|
||||||
|
} else {
|
||||||
|
a.distance_to(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cleanup_final_edges(&mut self) {
|
||||||
|
for node in &self.regular_nodes {
|
||||||
|
node.borrow_mut().final_edge = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter(&self) -> NodeStorageIterator {
|
||||||
|
self.regular_nodes.iter().chain(self.final_node.iter())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct Pathfinder {
|
||||||
|
#[wasm_bindgen(skip)]
|
||||||
|
pub nodes: NodeStorage,
|
||||||
|
#[wasm_bindgen(skip)]
|
||||||
|
pub walls: Vec<LineSegment>,
|
||||||
|
#[wasm_bindgen(skip)]
|
||||||
|
pub terrain: Vec<Rectangle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pathfinder {
|
||||||
|
pub fn initialize(
|
||||||
|
walls: Vec<Wall>,
|
||||||
|
terrain: Vec<Terrain>,
|
||||||
|
token_size: f64,
|
||||||
|
token_elevation: f64,
|
||||||
|
) -> Self {
|
||||||
|
log!("{:#?}", terrain);
|
||||||
|
let distance_from_walls = token_size / 2.0;
|
||||||
|
|
||||||
|
// TODO Place pathfinding nodes around terrain edges
|
||||||
|
let mut polygon_terrain = Vec::new();
|
||||||
|
let mut circle_terrain = Vec::new();
|
||||||
|
|
||||||
|
for terrain in &terrain {
|
||||||
|
match &terrain.shape {
|
||||||
|
TerrainShape::Polygon(polygon) => polygon_terrain.append(&mut polygon.clone()),
|
||||||
|
TerrainShape::Circle(circle) => circle_terrain.push(circle),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut walls = walls
|
||||||
|
.into_iter()
|
||||||
|
.filter(|wall| wall.move_type != WallSenseType::NONE)
|
||||||
|
.filter(|wall| !(wall.is_door() && wall.is_open()))
|
||||||
|
.filter(|wall| wall.height.contains(token_elevation))
|
||||||
|
.map(|wall| LineSegment::new(wall.p1, wall.p2))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// TODO Generate points around difficult terrain
|
||||||
|
let mut endpoints = FxHashMap::<Point, Vec<f64>>::default();
|
||||||
|
|
||||||
|
for segments in walls.iter().chain(polygon_terrain.iter()) {
|
||||||
|
let x_diff = segments.p2.x - segments.p1.x;
|
||||||
|
let y_diff = segments.p2.y - segments.p1.y;
|
||||||
|
let p1_angle = y_diff.atan2(x_diff).rem_euclid(2.0 * PI);
|
||||||
|
let p2_angle = (p1_angle + PI).rem_euclid(2.0 * PI);
|
||||||
|
for (point, angle) in [(segments.p1, p1_angle), (segments.p2, p2_angle)] {
|
||||||
|
let angles = endpoints.entry(point).or_insert_with(Vec::new);
|
||||||
|
angles.push(angle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endpoints
|
||||||
|
.values_mut()
|
||||||
|
.for_each(|angles| angles.sort_by(|a, b| a.partial_cmp(b).unwrap()));
|
||||||
|
let mut nodes = NodeStorage::new();
|
||||||
|
for (point, angles) in endpoints {
|
||||||
|
assert!(!angles.is_empty());
|
||||||
|
for i in 1..angles.len() {
|
||||||
|
let angle1 = angles[i - 1];
|
||||||
|
let angle2 = angles[i];
|
||||||
|
if angle1 == angle2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let angle_diff = angle2 - angle1;
|
||||||
|
if angle_diff <= PI {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let angle_between = angle_diff / 2.0 + angle1;
|
||||||
|
nodes.push(calc_pathfinding_node(
|
||||||
|
point,
|
||||||
|
angle_between,
|
||||||
|
distance_from_walls,
|
||||||
|
&mut walls,
|
||||||
|
));
|
||||||
|
nodes.push(calc_pathfinding_node(
|
||||||
|
point,
|
||||||
|
angle1 + 0.5 * PI,
|
||||||
|
distance_from_walls,
|
||||||
|
&mut walls,
|
||||||
|
));
|
||||||
|
nodes.push(calc_pathfinding_node(
|
||||||
|
point,
|
||||||
|
angle2 - 0.5 * PI,
|
||||||
|
distance_from_walls,
|
||||||
|
&mut walls,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let angle1 = angles.last().unwrap();
|
||||||
|
let angle2 = angles.first().unwrap() + 2.0 * PI;
|
||||||
|
let angle_diff = angle2 - angle1;
|
||||||
|
if angle_diff <= PI {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let angle_between = angle_diff / 2.0 + angle1;
|
||||||
|
nodes.push(calc_pathfinding_node(
|
||||||
|
point,
|
||||||
|
angle_between,
|
||||||
|
distance_from_walls,
|
||||||
|
&mut walls,
|
||||||
|
));
|
||||||
|
nodes.push(calc_pathfinding_node(
|
||||||
|
point,
|
||||||
|
angle1 + 0.5 * PI,
|
||||||
|
distance_from_walls,
|
||||||
|
&mut walls,
|
||||||
|
));
|
||||||
|
nodes.push(calc_pathfinding_node(
|
||||||
|
point,
|
||||||
|
angle2 - 0.5 * PI,
|
||||||
|
distance_from_walls,
|
||||||
|
&mut walls,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Check bounding box of circle terrain
|
||||||
|
for circle in circle_terrain {
|
||||||
|
let angle_step = f64::asin(token_size / 2.0 / circle.radius);
|
||||||
|
let mut angle = 0.0;
|
||||||
|
while angle < 2.0 * PI {
|
||||||
|
let point = Point {
|
||||||
|
x: circle.center.x + angle.cos() * circle.radius,
|
||||||
|
y: circle.center.y + angle.sin() * circle.radius,
|
||||||
|
};
|
||||||
|
nodes.push(calc_pathfinding_node(
|
||||||
|
point,
|
||||||
|
angle,
|
||||||
|
distance_from_walls,
|
||||||
|
&mut walls,
|
||||||
|
));
|
||||||
|
angle += angle_step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Eliminating nodes close to each other may improve performance
|
||||||
|
Self {
|
||||||
|
nodes,
|
||||||
|
walls,
|
||||||
|
terrain: terrain.iter().map(|terrain| terrain.bounding_box).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_path(&mut self, from: Point, to: Point) -> Option<DiscoveredNodePtr> {
|
||||||
|
self.nodes.cleanup_final_edges();
|
||||||
|
let mut nodes = self.nodes.clone();
|
||||||
|
nodes.final_node = Some(NodePtr::from(Node::new(from)));
|
||||||
|
let to_node = NodePtr::from(Node::new(to));
|
||||||
|
nodes.initialize_edges(&to_node, &self.walls, &self.terrain);
|
||||||
|
let to = DiscoveredNode {
|
||||||
|
node: to_node,
|
||||||
|
cost: 0.0,
|
||||||
|
estimated: to.distance_to(from),
|
||||||
|
previous: None,
|
||||||
|
};
|
||||||
|
// TODO Use a sorted set for next_nodes for better performance
|
||||||
|
let mut next_nodes = vec![DiscoveredNodePtr::from(to)];
|
||||||
|
let mut previous_nodes = PtrIndexedHashSet::new();
|
||||||
|
while !next_nodes.is_empty() {
|
||||||
|
// Sort by estimated cost, high to low
|
||||||
|
// TODO Maybe tere's a faster way to do this than re-sorting every iteration?
|
||||||
|
next_nodes.sort_by(|a, b| {
|
||||||
|
b.borrow()
|
||||||
|
.estimated
|
||||||
|
.partial_cmp(&a.borrow().estimated)
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get node with cheapest estimate
|
||||||
|
let current_node = next_nodes.pop().unwrap();
|
||||||
|
if current_node.borrow().node.borrow().point.x == from.x
|
||||||
|
&& current_node.borrow().node.borrow().point.y == from.y
|
||||||
|
{
|
||||||
|
return Some(current_node);
|
||||||
|
}
|
||||||
|
previous_nodes.insert(current_node.borrow().node.clone());
|
||||||
|
for edge in current_node.borrow().node.borrow().iter_edges() {
|
||||||
|
let neighbor = &edge.target;
|
||||||
|
if previous_nodes.contains(neighbor) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
nodes.initialize_edges(neighbor, &self.walls, &self.terrain);
|
||||||
|
// Add a flat 0.00001 cost per node to discurage creation of unnecessary waypoints
|
||||||
|
let cost = current_node.borrow().cost + edge.cost + 0.00001;
|
||||||
|
let discovered_neighbor = DiscoveredNode {
|
||||||
|
node: neighbor.clone(),
|
||||||
|
cost,
|
||||||
|
estimated: cost + neighbor.borrow().point.distance_to(from),
|
||||||
|
previous: Some(current_node.clone()),
|
||||||
|
};
|
||||||
|
let neighbor_entry = next_nodes
|
||||||
|
.iter()
|
||||||
|
.find(|node| Rc::ptr_eq(&node.borrow().node, neighbor));
|
||||||
|
if let Some(entry) = neighbor_entry {
|
||||||
|
// If the neighbor is cheaper to reach via the current route than through previously discovered routes, replace it
|
||||||
|
if entry.borrow().cost > cost {
|
||||||
|
*entry.borrow_mut() = discovered_neighbor;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
next_nodes.push(discovered_neighbor.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calc_pathfinding_node(
|
||||||
|
p: Point,
|
||||||
|
angle: f64,
|
||||||
|
distance_from_walls: f64,
|
||||||
|
walls: &mut Vec<LineSegment>,
|
||||||
|
) -> NodePtr {
|
||||||
|
let offset_x = angle.cos() * distance_from_walls;
|
||||||
|
let offset_y = angle.sin() * distance_from_walls;
|
||||||
|
walls.push(LineSegment::new(
|
||||||
|
p,
|
||||||
|
Point {
|
||||||
|
x: p.x + offset_x * 0.99,
|
||||||
|
y: p.y + offset_y * 0.99,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
NodePtr::from(Node::new(Point {
|
||||||
|
x: p.x + offset_x,
|
||||||
|
y: p.y + offset_y,
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
use rustc_hash::FxHashSet;
|
||||||
|
use std::collections::hash_set;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct PtrIndexedHashSet<T>(FxHashSet<PtrIndexedRc<T>>);
|
||||||
|
|
||||||
|
impl<T> PtrIndexedHashSet<T> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
PtrIndexedHashSet(FxHashSet::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, value: Rc<T>) -> bool {
|
||||||
|
self.0.insert(PtrIndexedRc(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, value: &Rc<T>) -> bool {
|
||||||
|
self.0.remove(&PtrIndexedRc(Rc::clone(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains(&mut self, value: &Rc<T>) -> bool {
|
||||||
|
self.0.contains(&PtrIndexedRc(Rc::clone(value)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: std::fmt::Debug> std::fmt::Debug for PtrIndexedHashSet<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
f.debug_set().entries(self.0.iter().map(|e| &e.0)).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PtrIndexedRc<T>(Rc<T>);
|
||||||
|
|
||||||
|
impl<T> Hash for PtrIndexedRc<T> {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
Rc::as_ptr(&self.0).hash(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> PartialEq for PtrIndexedRc<T> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
Rc::ptr_eq(&self.0, &other.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Eq for PtrIndexedRc<T> {}
|
||||||
|
|
||||||
|
pub struct PtrIndexedHashSetIterator<'a, T>(hash_set::Iter<'a, PtrIndexedRc<T>>);
|
||||||
|
|
||||||
|
impl<'a, T> Iterator for PtrIndexedHashSetIterator<'a, T> {
|
||||||
|
type Item = &'a Rc<T>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
match self.0.next() {
|
||||||
|
Some(item) => Some(&item.0),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> IntoIterator for &'a PtrIndexedHashSet<T> {
|
||||||
|
type Item = &'a Rc<T>;
|
||||||
|
type IntoIter = PtrIndexedHashSetIterator<'a, T>;
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
PtrIndexedHashSetIterator::<T>((&self.0).iter())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
pub struct WindowIterator<I: Iterator> {
|
||||||
|
iterator: I,
|
||||||
|
previous: Option<I::Item>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I: Iterator> Iterator for WindowIterator<I>
|
||||||
|
where
|
||||||
|
I::Item: Copy,
|
||||||
|
{
|
||||||
|
type Item = (I::Item, I::Item);
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.previous.is_none() {
|
||||||
|
self.previous = self.iterator.next();
|
||||||
|
}
|
||||||
|
let current = self.iterator.next();
|
||||||
|
if current.is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let current = current.unwrap();
|
||||||
|
let previous = self.previous.unwrap();
|
||||||
|
let result = (current, previous);
|
||||||
|
self.previous = Some(current);
|
||||||
|
Some(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Windows<T> {
|
||||||
|
fn windows(self) -> WindowIterator<Self>
|
||||||
|
where
|
||||||
|
Self: Sized + Iterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I: Iterator> Windows<I> for I {
|
||||||
|
fn windows(self) -> WindowIterator<Self>
|
||||||
|
where
|
||||||
|
Self: Sized + Iterator,
|
||||||
|
{
|
||||||
|
WindowIterator {
|
||||||
|
iterator: self,
|
||||||
|
previous: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-198
@@ -1,198 +0,0 @@
|
|||||||
import {measureDistances} from "./compatibility.js";
|
|
||||||
import {getMovementHistory} from "./movement_tracking.js";
|
|
||||||
import {GenericSpeedProvider, SpeedProvider} from "./speed_provider.js";
|
|
||||||
import {settingsKey} from "./settings.js";
|
|
||||||
import {getAreaFromPositionAndShape, getTokenShape} from "./util.js";
|
|
||||||
|
|
||||||
export const availableSpeedProviders = {};
|
|
||||||
export let currentSpeedProvider = undefined;
|
|
||||||
|
|
||||||
function register(module, type, speedProvider) {
|
|
||||||
const id = `${type}.${module.id}`;
|
|
||||||
let providerInstance;
|
|
||||||
if (speedProvider.prototype instanceof SpeedProvider) {
|
|
||||||
providerInstance = new speedProvider(id);
|
|
||||||
} else {
|
|
||||||
console.warn(
|
|
||||||
`Drag Ruler | The ${type} '${module.id}' uses the old, deprecated version of the Drag Ruler API. ` +
|
|
||||||
"That old API will be removed in a future Drag Ruler version. " +
|
|
||||||
`Please update the ${type} ${module.id} to stay compatible with future Drag Ruler versions.`,
|
|
||||||
);
|
|
||||||
speedProvider.id = id;
|
|
||||||
speedProvider.usesRuler = () => true;
|
|
||||||
providerInstance = speedProvider;
|
|
||||||
}
|
|
||||||
setupProvider(providerInstance);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupProvider(speedProvider) {
|
|
||||||
if (speedProvider instanceof SpeedProvider) {
|
|
||||||
const unreachableColor = {
|
|
||||||
id: "unreachable",
|
|
||||||
default: speedProvider.defaultUnreachableColor,
|
|
||||||
name: "drag-ruler.settings.speedProviderSettings.color.unreachable.name",
|
|
||||||
};
|
|
||||||
for (const color of speedProvider.colors.concat([unreachableColor])) {
|
|
||||||
game.settings.register(settingsKey, `speedProviders.${speedProvider.id}.color.${color.id}`, {
|
|
||||||
config: false,
|
|
||||||
scope: "client",
|
|
||||||
type: Number,
|
|
||||||
default: color.default,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
for (const setting of speedProvider.settings) {
|
|
||||||
setting.config = false;
|
|
||||||
game.settings.register(
|
|
||||||
settingsKey,
|
|
||||||
`speedProviders.${speedProvider.id}.setting.${setting.id}`,
|
|
||||||
setting,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
availableSpeedProviders[speedProvider.id] = speedProvider;
|
|
||||||
game.settings.settings.get("drag-ruler.speedProvider").default = getDefaultSpeedProvider();
|
|
||||||
updateSpeedProvider();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDefaultSpeedProvider() {
|
|
||||||
const providerIds = Object.keys(availableSpeedProviders);
|
|
||||||
// Game systems take the highest precedence for the being the default
|
|
||||||
const gameSystem = providerIds.find(key => key.startsWith("system."));
|
|
||||||
if (gameSystem) return gameSystem;
|
|
||||||
|
|
||||||
// If no game system is registered modules are next up.
|
|
||||||
// For lack of a method to select the best module we're just falling back to taking the next best module
|
|
||||||
// settingKeys should always be sorted the same way so this should achive a stable default
|
|
||||||
const module = providerIds.find(key => key.startsWith("module."));
|
|
||||||
if (module) return module;
|
|
||||||
|
|
||||||
// If neither a game system or a module is found fall back to the native implementation
|
|
||||||
return providerIds[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateSpeedProvider() {
|
|
||||||
// If the configured provider is registered use that one. If not use the default provider
|
|
||||||
const configuredProvider = game.settings.get("drag-ruler", "speedProvider");
|
|
||||||
currentSpeedProvider =
|
|
||||||
availableSpeedProviders[configuredProvider] ??
|
|
||||||
availableSpeedProviders[game.settings.settings.get("drag-ruler.speedProvider").default];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initApi() {
|
|
||||||
const genericSpeedProviderInstance = new GenericSpeedProvider("native");
|
|
||||||
setupProvider(genericSpeedProviderInstance);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRangesFromSpeedProvider(token) {
|
|
||||||
try {
|
|
||||||
if (currentSpeedProvider instanceof Function) return currentSpeedProvider(token, 0x00ff00);
|
|
||||||
const ranges = currentSpeedProvider.getRanges(token);
|
|
||||||
for (const range of ranges) {
|
|
||||||
range.color = game.settings.get(
|
|
||||||
settingsKey,
|
|
||||||
`speedProviders.${currentSpeedProvider.id}.color.${range.color}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return ranges;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUnreachableColorFromSpeedProvider() {
|
|
||||||
if (currentSpeedProvider instanceof Function) return 0xff0000;
|
|
||||||
try {
|
|
||||||
return game.settings.get(
|
|
||||||
settingsKey,
|
|
||||||
`speedProviders.${currentSpeedProvider.id}.color.unreachable`,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
return 0xff0000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getColorForDistanceAndToken(distance, token, ranges = null) {
|
|
||||||
if (!ranges) {
|
|
||||||
ranges = getRangesFromSpeedProvider(token);
|
|
||||||
}
|
|
||||||
if (ranges.length === 0) return null;
|
|
||||||
const currentRange = ranges.reduce(
|
|
||||||
(minRange, currentRange) => {
|
|
||||||
if (distance <= currentRange.range && currentRange.range < minRange.range)
|
|
||||||
return currentRange;
|
|
||||||
return minRange;
|
|
||||||
},
|
|
||||||
{range: Infinity, color: getUnreachableColorFromSpeedProvider()},
|
|
||||||
);
|
|
||||||
return currentRange.color;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMovedDistanceFromToken(token) {
|
|
||||||
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active;
|
|
||||||
const history = getMovementHistory(token);
|
|
||||||
const segments = CONFIG.Canvas.rulerClass
|
|
||||||
.dragRulerGetRaysFromWaypoints(history, {x: token.x, y: token.y})
|
|
||||||
.map(ray => {
|
|
||||||
return {ray};
|
|
||||||
});
|
|
||||||
const shape = getTokenShape(token);
|
|
||||||
const distances = measureDistances(segments, token, shape, {
|
|
||||||
enableTerrainRuler: terrainRulerAvailable,
|
|
||||||
});
|
|
||||||
// Sum up the distances
|
|
||||||
return distances.reduce((acc, val) => acc + val, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildCostFunction(token, shape) {
|
|
||||||
return (x, y, costOptions = {}) => {
|
|
||||||
costOptions.token = token;
|
|
||||||
const area = getAreaFromPositionAndShape({x, y}, shape);
|
|
||||||
const costs = area.map(space => canvas.terrain.cost({x: space.x, y: space.y, costOptions}));
|
|
||||||
return costs.reduce((max, current) => Math.max(max, current));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function registerModule(moduleId, speedProvider) {
|
|
||||||
// Check if a module with the given id exists and is currently enabled
|
|
||||||
const module = game.modules.get(moduleId);
|
|
||||||
// If it doesn't the calling module did something wrong. Log a warning and ignore this module
|
|
||||||
if (!module) {
|
|
||||||
console.warn(
|
|
||||||
`Drag Ruler | A module tried to register with the id "${moduleId}". However no active module with this id was found.` +
|
|
||||||
"This api registration call was ignored. " +
|
|
||||||
"If you are the author of that module please check that the id passed to `registerModule` matches the id in your manifest exactly." +
|
|
||||||
"If this call was made form a game system instead of a module please use `registerSystem` instead.",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Using Drag Ruler's id is not allowed
|
|
||||||
if (moduleId === "drag-ruler") {
|
|
||||||
console.warn(
|
|
||||||
`Drag Ruler | A module tried to register with the id "${moduleId}", which is not allowed. This api registration call was ignored. ` +
|
|
||||||
"If you're the author of the module please use the id of your own module as it's specified in your manifest to register to this api. " +
|
|
||||||
"If this call was made form a game system instead of a module please use `registerSystem` instead.",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
register(module, "module", speedProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function registerSystem(systemId, speedProvider) {
|
|
||||||
const system = game.system;
|
|
||||||
// If the current system id doesn't match the provided id something went wrong. Log a warning and ignore this module
|
|
||||||
if (system.id != systemId) {
|
|
||||||
console.warn(
|
|
||||||
`Drag Ruler | A system tried to register with the id "${systemId}". However the active system has a different id.` +
|
|
||||||
"This api registration call was ignored. " +
|
|
||||||
"If you are the author of that system please check that the id passed to `registerSystem` matches the id in your manifest exactly." +
|
|
||||||
"If this call was made form a module instead of a game system please use `registerModule` instead.",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
register(system, "system", speedProvider);
|
|
||||||
}
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
import {getGridPositionFromPixels} from "./foundry_fixes.js";
|
|
||||||
import {disableSnap, moveWithoutAnimation} from "./keybindings.js";
|
|
||||||
import {trackRays} from "./movement_tracking.js";
|
|
||||||
import {recalculate} from "./socket.js";
|
|
||||||
import {getSnapPointForToken, highlightTokenShape, sum} from "./util.js";
|
|
||||||
|
|
||||||
// This is a modified version of Ruler.moveToken from foundry 0.7.9
|
|
||||||
export async function moveEntities(draggedEntity, selectedEntities) {
|
|
||||||
let wasPaused = game.paused;
|
|
||||||
if (wasPaused && !game.user.isGM) {
|
|
||||||
ui.notifications.warn(game.i18n.localize("GAME.PausedWarning"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!this.visible || !this.destination) return false;
|
|
||||||
if (!draggedEntity) return;
|
|
||||||
|
|
||||||
// Wait until all scheduled measurements are done
|
|
||||||
await this.deferredMeasurementPromise;
|
|
||||||
|
|
||||||
// Get the movement rays and check collision along each Ray
|
|
||||||
// These rays are center-to-center for the purposes of collision checking
|
|
||||||
const rays = this.constructor.dragRulerGetRaysFromWaypoints(this.waypoints, this.destination);
|
|
||||||
if (!game.user.isGM && draggedEntity instanceof Token) {
|
|
||||||
const hasCollision = selectedEntities.some(token => {
|
|
||||||
const offset = calculateEntityOffset(token, draggedEntity);
|
|
||||||
const offsetRays = rays
|
|
||||||
.filter(ray => !ray.isPrevious)
|
|
||||||
.map(ray => applyOffsetToRay(ray, offset));
|
|
||||||
return offsetRays.some(r =>
|
|
||||||
token.checkCollision(r.B, {
|
|
||||||
origin: r.A,
|
|
||||||
mode: "any",
|
|
||||||
type: "move",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if (hasCollision) {
|
|
||||||
ui.notifications.error(game.i18n.localize("ERROR.TokenCollide"));
|
|
||||||
this._endMeasurement();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute the movement path.
|
|
||||||
// Transform each center-to-center ray into a top-left to top-left ray using the prior token offsets.
|
|
||||||
this._state = Ruler.STATES.MOVING;
|
|
||||||
await animateEntities.call(this, selectedEntities, draggedEntity, rays, wasPaused);
|
|
||||||
|
|
||||||
// Once all animations are complete we can clear the ruler
|
|
||||||
if (this.draggedEntity?.id === draggedEntity.id) this._endMeasurement();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a modified version code extracted from Ruler.moveToken from foundry 0.7.9
|
|
||||||
async function animateEntities(entities, draggedEntity, draggedRays, wasPaused) {
|
|
||||||
const newRays = draggedRays.filter(r => !r.isPrevious);
|
|
||||||
const entityAnimationData = entities.map(entity => {
|
|
||||||
const entityOffset = calculateEntityOffset(entity, draggedEntity);
|
|
||||||
const offsetRays = newRays.map(ray => applyOffsetToRay(ray, entityOffset));
|
|
||||||
|
|
||||||
// Determine offset relative to the Token top-left.
|
|
||||||
// This is important so we can position the token relative to the ruler origin for non-1x1 tokens.
|
|
||||||
const firstWaypoint = this.waypoints.find(w => !w.isPrevious);
|
|
||||||
const origin = [firstWaypoint.x + entityOffset.x, firstWaypoint.y + entityOffset.y];
|
|
||||||
let dx, dy;
|
|
||||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
|
||||||
dx = entity.x - origin[0];
|
|
||||||
dy = entity.y - origin[1];
|
|
||||||
} else {
|
|
||||||
dx = entity.x - origin[0];
|
|
||||||
dy = entity.y - origin[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {entity, rays: offsetRays, dx, dy};
|
|
||||||
});
|
|
||||||
|
|
||||||
const isToken = draggedEntity instanceof Token;
|
|
||||||
const animate = isToken && !moveWithoutAnimation;
|
|
||||||
const startWaypoint = animate ? 0 : entityAnimationData[0].rays.length - 1;
|
|
||||||
|
|
||||||
// This is a flag of the "Monk's Active Tile Triggers" module that signals that the movement should be cancelled early
|
|
||||||
this.cancelMovement = false;
|
|
||||||
|
|
||||||
for (let i = startWaypoint; i < entityAnimationData[0].rays.length; i++) {
|
|
||||||
if (!wasPaused && game.paused) break;
|
|
||||||
const entityPaths = entityAnimationData.map(({entity, rays, dx, dy}) => {
|
|
||||||
const ray = rays[i];
|
|
||||||
const dest = [ray.B.x, ray.B.y];
|
|
||||||
const path = new Ray({x: entity.x, y: entity.y}, {x: dest[0] + dx, y: dest[1] + dy});
|
|
||||||
return {entity, path};
|
|
||||||
});
|
|
||||||
const updates = entityPaths.map(({entity, path}) => {
|
|
||||||
return {x: path.B.x, y: path.B.y, _id: entity.id};
|
|
||||||
});
|
|
||||||
await draggedEntity.scene.updateEmbeddedDocuments(
|
|
||||||
draggedEntity.constructor.embeddedName,
|
|
||||||
updates,
|
|
||||||
{animate},
|
|
||||||
);
|
|
||||||
if (animate)
|
|
||||||
await Promise.all(
|
|
||||||
entityPaths.map(({entity}) => CanvasAnimation.getAnimation(entity.animationName)?.promise),
|
|
||||||
);
|
|
||||||
|
|
||||||
// This is a flag of the "Monk's Active Tile Triggers" module that signals that the movement should be cancelled early
|
|
||||||
if (this.cancelMovement) {
|
|
||||||
entityAnimationData.forEach(ead => (ead.rays = ead.rays.slice(0, i + 1)));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isToken)
|
|
||||||
trackRays(
|
|
||||||
entities,
|
|
||||||
entityAnimationData.map(({rays}) => rays),
|
|
||||||
).then(() => recalculate(entities));
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateEntityOffset(entityA, entityB) {
|
|
||||||
return {x: entityA.x - entityB.x, y: entityA.y - entityB.y};
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyOffsetToRay(ray, offset) {
|
|
||||||
const newRay = new Ray(
|
|
||||||
{x: ray.A.x + offset.x, y: ray.A.y + offset.y},
|
|
||||||
{x: ray.B.x + offset.x, y: ray.B.y + offset.y},
|
|
||||||
);
|
|
||||||
newRay.isPrevious = ray.isPrevious;
|
|
||||||
return newRay;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a modified version of Ruler._onMouseMove from foundry 0.7.9
|
|
||||||
export function onMouseMove(event) {
|
|
||||||
if (this._state === Ruler.STATES.MOVING) return;
|
|
||||||
|
|
||||||
// Extract event data
|
|
||||||
const destination = {
|
|
||||||
x: event.data.destination.x + this.rulerOffset.x,
|
|
||||||
y: event.data.destination.y + this.rulerOffset.y,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Hide any existing Token HUD
|
|
||||||
canvas.hud.token.clear();
|
|
||||||
delete event.data.hudState;
|
|
||||||
|
|
||||||
// Draw measurement updates
|
|
||||||
scheduleMeasurement.call(this, destination, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleMeasurement(destination, event) {
|
|
||||||
const measurementInterval = 50;
|
|
||||||
const mt = event._measureTime || 0;
|
|
||||||
const originalEvent = event.data.originalEvent;
|
|
||||||
if (Date.now() - mt > measurementInterval) {
|
|
||||||
this.measure(destination, {snap: !disableSnap});
|
|
||||||
event._measureTime = Date.now();
|
|
||||||
this._state = Ruler.STATES.MEASURING;
|
|
||||||
cancelScheduledMeasurement.call(this);
|
|
||||||
} else {
|
|
||||||
this.deferredMeasurementData = {destination, event};
|
|
||||||
if (!this.deferredMeasurementTimeout) {
|
|
||||||
this.deferredMeasurementPromise = new Promise(
|
|
||||||
(resolve, reject) => (this.deferredMeasurementResolve = resolve),
|
|
||||||
);
|
|
||||||
this.deferredMeasurementTimeout = window.setTimeout(
|
|
||||||
() =>
|
|
||||||
scheduleMeasurement.call(
|
|
||||||
this,
|
|
||||||
this.deferredMeasurementData.destination,
|
|
||||||
this.deferredMeasurementData.event,
|
|
||||||
),
|
|
||||||
measurementInterval,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cancelScheduledMeasurement() {
|
|
||||||
window.clearTimeout(this.deferredMeasurementTimeout);
|
|
||||||
this.deferredMeasurementTimeout = undefined;
|
|
||||||
this.deferredMeasurementResolve?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function highlightMeasurementNative(
|
|
||||||
ray,
|
|
||||||
previousSegments,
|
|
||||||
tokenShape = [{x: 0, y: 0}],
|
|
||||||
alpha = 1,
|
|
||||||
) {
|
|
||||||
const spacer = canvas.scene.gridType === CONST.GRID_TYPES.SQUARE ? 1.41 : 1;
|
|
||||||
const nMax = Math.max(
|
|
||||||
Math.floor(ray.distance / (spacer * Math.min(canvas.grid.w, canvas.grid.h))),
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
const tMax = Array.fromRange(nMax + 1).map(t => t / nMax);
|
|
||||||
|
|
||||||
// Track prior position
|
|
||||||
let prior = null;
|
|
||||||
|
|
||||||
// Iterate over ray portions
|
|
||||||
for (let [i, t] of tMax.reverse().entries()) {
|
|
||||||
let {x, y} = ray.project(t);
|
|
||||||
|
|
||||||
// Get grid position
|
|
||||||
let [x0, y0] = i === 0 ? [null, null] : prior;
|
|
||||||
let [x1, y1] = canvas.grid.grid.getGridPositionFromPixels(x, y);
|
|
||||||
if (x0 === x1 && y0 === y1) continue;
|
|
||||||
|
|
||||||
// Highlight the grid position
|
|
||||||
let [xgtl, ygtl] = canvas.grid.grid.getPixelsFromGridPosition(x1, y1);
|
|
||||||
let [xg, yg] = canvas.grid.grid.getCenter(xgtl, ygtl);
|
|
||||||
const pathUntilSpace = previousSegments.concat([{ray: new Ray(ray.A, {x: xg, y: yg})}]);
|
|
||||||
const distance = sum(canvas.grid.measureDistances(pathUntilSpace, {gridSpaces: true}));
|
|
||||||
const color = this.dragRulerGetColorForDistance(distance);
|
|
||||||
const snapPoint = getSnapPointForToken(...canvas.grid.getTopLeft(x, y), this.draggedEntity);
|
|
||||||
const [snapX, snapY] = getGridPositionFromPixels(snapPoint.x + 1, snapPoint.y + 1);
|
|
||||||
|
|
||||||
prior = [x1, y1];
|
|
||||||
|
|
||||||
// If the positions are not neighbors, also highlight their halfway point
|
|
||||||
if (i > 0 && !canvas.grid.isNeighbor(x0, y0, x1, y1)) {
|
|
||||||
let th = tMax[i - 1] - 0.5 / nMax;
|
|
||||||
let {x, y} = ray.project(th);
|
|
||||||
let [x1h, y1h] = canvas.grid.grid.getGridPositionFromPixels(x, y);
|
|
||||||
let [xghtl, yghtl] = canvas.grid.grid.getPixelsFromGridPosition(x1h, y1h);
|
|
||||||
let [xgh, ygh] = canvas.grid.grid.getCenter(xghtl, yghtl);
|
|
||||||
const pathUntilSpace = previousSegments.concat([{ray: new Ray(ray.A, {x: xgh, y: ygh})}]);
|
|
||||||
const distance = sum(canvas.grid.measureDistances(pathUntilSpace, {gridSpaces: true}));
|
|
||||||
const color = this.dragRulerGetColorForDistance(distance);
|
|
||||||
const snapPoint = getSnapPointForToken(...canvas.grid.getTopLeft(x, y), this.draggedEntity);
|
|
||||||
const [snapX, snapY] = getGridPositionFromPixels(snapPoint.x + 1, snapPoint.y + 1);
|
|
||||||
highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color, alpha);
|
|
||||||
}
|
|
||||||
|
|
||||||
highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color, alpha);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
// Copyright © 2021 fvtt-lib-wrapper Rui Pinheiro
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// A shim for the libWrapper library
|
|
||||||
export let libWrapper = undefined;
|
|
||||||
|
|
||||||
export const VERSIONS = [1, 11, 0];
|
|
||||||
export const TGT_SPLIT_RE = new RegExp(
|
|
||||||
"([^.[]+|\\[('([^'\\\\]|\\\\.)+?'|\"([^\"\\\\]|\\\\.)+?\")\\])",
|
|
||||||
"g",
|
|
||||||
);
|
|
||||||
export const TGT_CLEANUP_RE = new RegExp("(^\\['|'\\]$|^\\[\"|\"\\]$)", "g");
|
|
||||||
|
|
||||||
// Main shim code
|
|
||||||
Hooks.once("init", () => {
|
|
||||||
// Check if the real module is already loaded - if so, use it
|
|
||||||
if (globalThis.libWrapper && !(globalThis.libWrapper.is_fallback ?? true)) {
|
|
||||||
libWrapper = globalThis.libWrapper;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback implementation
|
|
||||||
libWrapper = class {
|
|
||||||
static get is_fallback() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get WRAPPER() {
|
|
||||||
return "WRAPPER";
|
|
||||||
}
|
|
||||||
static get MIXED() {
|
|
||||||
return "MIXED";
|
|
||||||
}
|
|
||||||
static get OVERRIDE() {
|
|
||||||
return "OVERRIDE";
|
|
||||||
}
|
|
||||||
|
|
||||||
static register(package_id, target, fn, type = "MIXED", {chain = undefined} = {}) {
|
|
||||||
const is_setter = target.endsWith("#set");
|
|
||||||
target = !is_setter ? target : target.slice(0, -4);
|
|
||||||
const split = target
|
|
||||||
.match(TGT_SPLIT_RE)
|
|
||||||
.map(x => x.replace(/\\(.)/g, "$1").replace(TGT_CLEANUP_RE, ""));
|
|
||||||
const root_nm = split.splice(0, 1)[0];
|
|
||||||
|
|
||||||
let obj, fn_name;
|
|
||||||
if (split.length == 0) {
|
|
||||||
obj = globalThis;
|
|
||||||
fn_name = root_nm;
|
|
||||||
} else {
|
|
||||||
const _eval = eval;
|
|
||||||
fn_name = split.pop();
|
|
||||||
obj = split.reduce((x, y) => x[y], globalThis[root_nm] ?? _eval(root_nm));
|
|
||||||
}
|
|
||||||
|
|
||||||
let iObj = obj;
|
|
||||||
let descriptor = null;
|
|
||||||
while (iObj) {
|
|
||||||
descriptor = Object.getOwnPropertyDescriptor(iObj, fn_name);
|
|
||||||
if (descriptor) break;
|
|
||||||
iObj = Object.getPrototypeOf(iObj);
|
|
||||||
}
|
|
||||||
if (!descriptor || descriptor?.configurable === false)
|
|
||||||
throw `libWrapper Shim: '${target}' does not exist, could not be found, or has a non-configurable descriptor.`;
|
|
||||||
|
|
||||||
let original = null;
|
|
||||||
const wrapper =
|
|
||||||
chain ?? (type.toUpperCase?.() != "OVERRIDE" && type != 3)
|
|
||||||
? function () {
|
|
||||||
return fn.call(this, original.bind(this), ...arguments);
|
|
||||||
}
|
|
||||||
: function () {
|
|
||||||
return fn.apply(this, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!is_setter) {
|
|
||||||
if (descriptor.value) {
|
|
||||||
original = descriptor.value;
|
|
||||||
descriptor.value = wrapper;
|
|
||||||
} else {
|
|
||||||
original = descriptor.get;
|
|
||||||
descriptor.get = wrapper;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!descriptor.set) throw `libWrapper Shim: '${target}' does not have a setter`;
|
|
||||||
original = descriptor.set;
|
|
||||||
descriptor.set = wrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
descriptor.configurable = true;
|
|
||||||
Object.defineProperty(obj, fn_name, descriptor);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
-515
@@ -1,515 +0,0 @@
|
|||||||
import {
|
|
||||||
currentSpeedProvider,
|
|
||||||
getColorForDistanceAndToken,
|
|
||||||
getRangesFromSpeedProvider,
|
|
||||||
} from "./api.js";
|
|
||||||
import {
|
|
||||||
getHexSizeSupportTokenGridCenter,
|
|
||||||
highlightMeasurementTerrainRuler,
|
|
||||||
measureDistances,
|
|
||||||
} from "./compatibility.js";
|
|
||||||
import {getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
|
|
||||||
import {cancelScheduledMeasurement, highlightMeasurementNative} from "./foundry_imports.js";
|
|
||||||
import {disableSnap} from "./keybindings.js";
|
|
||||||
import {getMovementHistory} from "./movement_tracking.js";
|
|
||||||
import {settingsKey} from "./settings.js";
|
|
||||||
import {
|
|
||||||
applyTokenSizeOffset,
|
|
||||||
getSnapPointForEntity,
|
|
||||||
getSnapPointForTokenObj,
|
|
||||||
getTokenShape,
|
|
||||||
isPathfindingEnabled,
|
|
||||||
} from "./util.js";
|
|
||||||
|
|
||||||
export function extendRuler() {
|
|
||||||
class DragRulerRuler extends CONFIG.Canvas.rulerClass {
|
|
||||||
// Functions below are overridden versions of functions in Ruler
|
|
||||||
constructor(user, {color = null} = {}) {
|
|
||||||
super(user, {color});
|
|
||||||
this.previousWaypoints = [];
|
|
||||||
this.previousLabels = this.addChild(new PIXI.Container());
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
super.clear();
|
|
||||||
this.previousWaypoints = [];
|
|
||||||
this.previousLabels.removeChildren().forEach(c => c.destroy());
|
|
||||||
this.dragRulerRanges = undefined;
|
|
||||||
cancelScheduledMeasurement.call(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
async moveToken(event) {
|
|
||||||
// Disable moveToken if Drag Ruler is active
|
|
||||||
if (!this.isDragRuler) return await super.moveToken(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON() {
|
|
||||||
const json = super.toJSON();
|
|
||||||
if (this.draggedEntity) {
|
|
||||||
const isToken = this.draggedEntity instanceof Token;
|
|
||||||
json.draggedEntityIsToken = isToken;
|
|
||||||
json.draggedEntity = this.draggedEntity.id;
|
|
||||||
json.waypoints = json.waypoints.map(old => {
|
|
||||||
let w = duplicate(old);
|
|
||||||
w.isPathfinding = undefined;
|
|
||||||
return w;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
update(data) {
|
|
||||||
// Don't show a GMs drag ruler to non GM players
|
|
||||||
if (
|
|
||||||
data.draggedEntity &&
|
|
||||||
this.user.isGM &&
|
|
||||||
!game.user.isGM &&
|
|
||||||
!game.settings.get(settingsKey, "showGMRulerToPlayers")
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (data.draggedEntity) {
|
|
||||||
if (data.draggedEntityIsToken) this.draggedEntity = canvas.tokens.get(data.draggedEntity);
|
|
||||||
else this.draggedEntity = canvas.templates.get(data.draggedEntity);
|
|
||||||
} else {
|
|
||||||
this.draggedEntity = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
super.update(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
measure(destination, options = {}) {
|
|
||||||
if (!this.isDragRuler) {
|
|
||||||
return super.measure(destination, options);
|
|
||||||
}
|
|
||||||
if (options.gridSpaces === undefined) {
|
|
||||||
options.gridSpaces = canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS;
|
|
||||||
}
|
|
||||||
this.dragRulerGridSpaces = options.gridSpaces;
|
|
||||||
const isToken = this.draggedEntity instanceof Token;
|
|
||||||
if (isToken && !this.draggedEntity.isVisible) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
if (canvas.grid.diagonalRule === "EUCL") {
|
|
||||||
options.gridSpaces = false;
|
|
||||||
options.ignoreGrid = true;
|
|
||||||
}
|
|
||||||
if (options.ignoreGrid === undefined) {
|
|
||||||
options.ignoreGrid = false;
|
|
||||||
}
|
|
||||||
this.dragRulerIgnoreGrid = options.ignoreGrid;
|
|
||||||
// If this is the ruler of a remote user take the waypoints as they were transmitted and don't apply any additional snapping to them
|
|
||||||
if (this.user !== game.user) {
|
|
||||||
options.snap = false;
|
|
||||||
}
|
|
||||||
this.dragRulerSnap = options.snap ?? !disableSnap;
|
|
||||||
this.dragRulerEnableTerrainRuler = isToken && window.terrainRuler;
|
|
||||||
|
|
||||||
// Compute the measurement destination, segments, and distance
|
|
||||||
const d = this._getMeasurementDestination(destination);
|
|
||||||
if (d.x === this.destination.x && d.y === this.destination.y) return;
|
|
||||||
this.destination = d;
|
|
||||||
|
|
||||||
// TODO Check if we can reuse the old path
|
|
||||||
this.dragRulerRemovePathfindingWaypoints();
|
|
||||||
if (this.pathfindingJob) {
|
|
||||||
routinglib.cancelPathfinding(this.pathfindingJob);
|
|
||||||
this.pathfindingJob = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isToken && isPathfindingEnabled.call(this)) {
|
|
||||||
// TODO Show a busy indicator
|
|
||||||
const from = getGridPositionFromPixelsObj(this.waypoints[this.waypoints.length - 1]);
|
|
||||||
const to = getGridPositionFromPixelsObj(destination);
|
|
||||||
|
|
||||||
const pathfindingJob = routinglib.calculatePath(from, to, {token: this.draggedEntity});
|
|
||||||
this.pathfindingJob = pathfindingJob;
|
|
||||||
return this.pathfindingJob.then(result => {
|
|
||||||
if (pathfindingJob === this.pathfindingJob) {
|
|
||||||
this.pathfindingJob = undefined;
|
|
||||||
this.addPathToWaypoints(result?.path);
|
|
||||||
return this.performPostPathfindingActions(options);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.performPostPathfindingActions(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
addPathToWaypoints(path) {
|
|
||||||
if (!path) {
|
|
||||||
// TODO Show an indicator informing that there is no path
|
|
||||||
|
|
||||||
// Don't show a path if the pathfinding yields no result to show the user that the destination is unreachable
|
|
||||||
this.destination = this.waypoints[this.waypoints.length - 1];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
path = path.map(point =>
|
|
||||||
getSnapPointForTokenObj(getPixelsFromGridPositionObj(point), this.draggedEntity),
|
|
||||||
);
|
|
||||||
|
|
||||||
// If the token is snapped to the grid, the first point of the path is already handled by the ruler
|
|
||||||
if (
|
|
||||||
path[0].x === this.waypoints[this.waypoints.length - 1].x &&
|
|
||||||
path[0].y === this.waypoints[this.waypoints.length - 1].y
|
|
||||||
) {
|
|
||||||
path = path.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If snapping is enabled, the last point of the path is already handled by the ruler
|
|
||||||
if (this.dragRulerSnap) {
|
|
||||||
path = path.slice(0, path.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const point of path) {
|
|
||||||
point.isPathfinding = true;
|
|
||||||
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
|
|
||||||
}
|
|
||||||
this.waypoints = this.waypoints.concat(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
performPostPathfindingActions(options) {
|
|
||||||
// TODO Clear pathfinding busy indicator
|
|
||||||
this.segments = this._getMeasurementSegments();
|
|
||||||
this._computeDistance(options.gridSpaces);
|
|
||||||
|
|
||||||
// Draw the ruler graphic
|
|
||||||
this.ruler.clear();
|
|
||||||
this._drawMeasuredPath();
|
|
||||||
|
|
||||||
// Draw grid highlight
|
|
||||||
this.highlightLayer.clear();
|
|
||||||
const isToken = this.draggedEntity instanceof Token;
|
|
||||||
if (isToken && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS && this.dragRulerGridSpaces) {
|
|
||||||
const shape = getTokenShape(this.draggedEntity);
|
|
||||||
if (!this.dragRulerEnableTerrainRuler) {
|
|
||||||
for (const [i, segment] of [...this.segments].reverse().entries()) {
|
|
||||||
const opacityMultiplier = segment.ray.isPrevious ? 0.33 : 1;
|
|
||||||
const previousSegments = this.segments.slice(0, this.segments.length - 1 - i);
|
|
||||||
highlightMeasurementNative.call(
|
|
||||||
this,
|
|
||||||
segment.ray,
|
|
||||||
previousSegments,
|
|
||||||
shape,
|
|
||||||
opacityMultiplier,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (const segment of [...this.segments].reverse()) {
|
|
||||||
const opacityMultiplier = segment.ray.isPrevious ? 0.33 : 1;
|
|
||||||
highlightMeasurementTerrainRuler.call(
|
|
||||||
this,
|
|
||||||
segment.ray,
|
|
||||||
segment.startDistance,
|
|
||||||
shape,
|
|
||||||
opacityMultiplier,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.segments;
|
|
||||||
}
|
|
||||||
|
|
||||||
_getMeasurementDestination(destination) {
|
|
||||||
if (this.isDragRuler) {
|
|
||||||
if (this.dragRulerSnap) {
|
|
||||||
return getSnapPointForEntity(destination.x, destination.y, this.draggedEntity);
|
|
||||||
} else {
|
|
||||||
return destination;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return super._getMeasurementDestination(destination);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_getMeasurementSegments() {
|
|
||||||
if (this.isDragRuler) {
|
|
||||||
const unsnappedWaypoints = this.waypoints.concat([this.destination]);
|
|
||||||
const waypoints =
|
|
||||||
this.draggedEntity instanceof Token
|
|
||||||
? applyTokenSizeOffset(unsnappedWaypoints, this.draggedEntity)
|
|
||||||
: duplicate(unsnappedWaypoints);
|
|
||||||
const unsnappedSegments = [];
|
|
||||||
const segments = [];
|
|
||||||
for (const [i, p1] of waypoints.entries()) {
|
|
||||||
if (i === 0) continue;
|
|
||||||
const unsnappedP1 = unsnappedWaypoints[i];
|
|
||||||
const p0 = waypoints[i - 1];
|
|
||||||
const unsnappedP0 = unsnappedWaypoints[i - 1];
|
|
||||||
const label = this.labels.children[i - 1];
|
|
||||||
const ray = new Ray(p0, p1);
|
|
||||||
const unsnappedRay = new Ray(unsnappedP0, unsnappedP1);
|
|
||||||
ray.isPrevious = Boolean(unsnappedP0.isPrevious);
|
|
||||||
unsnappedRay.isPrevious = ray.isPrevious;
|
|
||||||
ray.dragRulerVisitedSpaces = unsnappedP0.dragRulerVisitedSpaces;
|
|
||||||
unsnappedRay.dragRulerVisitedSpaces = ray.dragRulerVisitedSpaces;
|
|
||||||
ray.dragRulerFinalState = unsnappedP0.dragRulerFinalState;
|
|
||||||
unsnappedRay.dragRulerFinalState = ray.dragRulerFinalState;
|
|
||||||
if (ray.distance < 10) {
|
|
||||||
if (label) label.visible = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
segments.push({ray, label});
|
|
||||||
unsnappedSegments.push({ray: unsnappedRay, label});
|
|
||||||
}
|
|
||||||
this.dragRulerUnsnappedSegments = unsnappedSegments;
|
|
||||||
return segments;
|
|
||||||
} else {
|
|
||||||
return super._getMeasurementSegments();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeDistance(gridSpaces) {
|
|
||||||
if (!this.isDragRuler) {
|
|
||||||
return super._computeDistance(gridSpaces);
|
|
||||||
}
|
|
||||||
if (!this.dragRulerEnableTerrainRuler) {
|
|
||||||
if (!this.dragRulerIgnoreGrid) {
|
|
||||||
gridSpaces = true;
|
|
||||||
}
|
|
||||||
super._computeDistance(gridSpaces);
|
|
||||||
} else {
|
|
||||||
const shape = this.draggedEntity ? getTokenShape(this.draggedEntity) : null;
|
|
||||||
const options = {
|
|
||||||
ignoreGrid: this.dragRulerIgnoreGrid,
|
|
||||||
gridSpaces,
|
|
||||||
enableTerrainRuler: this.dragRulerEnableTerrainRuler,
|
|
||||||
};
|
|
||||||
const distances = measureDistances(this.segments, this.draggedEntity, shape, options);
|
|
||||||
let totalDistance = 0;
|
|
||||||
for (const [i, d] of distances.entries()) {
|
|
||||||
let s = this.segments[i];
|
|
||||||
s.startDistance = totalDistance;
|
|
||||||
totalDistance += d;
|
|
||||||
s.last = i === this.segments.length - 1;
|
|
||||||
s.distance = d;
|
|
||||||
s.text = this._getSegmentLabel(s, totalDistance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const [i, segment] of this.segments.entries()) {
|
|
||||||
const unsnappedSegment = this.dragRulerUnsnappedSegments[i];
|
|
||||||
unsnappedSegment.startDistance = segment.startDistance;
|
|
||||||
unsnappedSegment.last = segment.last;
|
|
||||||
unsnappedSegment.distance = segment.distance;
|
|
||||||
unsnappedSegment.text = segment.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_drawMeasuredPath() {
|
|
||||||
if (!this.isDragRuler) {
|
|
||||||
return super._drawMeasuredPath();
|
|
||||||
}
|
|
||||||
let rulerColor = this.color;
|
|
||||||
if (!this.dragRulerGridSpaces || canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
|
||||||
const totalDistance = this.segments.reduce((total, current) => total + current.distance, 0);
|
|
||||||
rulerColor = this.dragRulerGetColorForDistance(totalDistance);
|
|
||||||
}
|
|
||||||
const r = this.ruler.beginFill(rulerColor, 0.25);
|
|
||||||
for (const segment of this.dragRulerUnsnappedSegments) {
|
|
||||||
const opacityMultiplier = segment.ray.isPrevious ? 0.33 : 1;
|
|
||||||
const {ray, distance, label, text, last} = segment;
|
|
||||||
if (distance === 0) continue;
|
|
||||||
|
|
||||||
// Draw Line
|
|
||||||
r.moveTo(ray.A.x, ray.A.y)
|
|
||||||
.lineStyle(6, 0x000000, 0.5 * opacityMultiplier)
|
|
||||||
.lineTo(ray.B.x, ray.B.y)
|
|
||||||
.lineStyle(4, rulerColor, 0.25 * opacityMultiplier)
|
|
||||||
.moveTo(ray.A.x, ray.A.y)
|
|
||||||
.lineTo(ray.B.x, ray.B.y);
|
|
||||||
|
|
||||||
// Draw Waypoints
|
|
||||||
r.lineStyle(2, 0x000000, 0.5).drawCircle(ray.A.x, ray.A.y, 8);
|
|
||||||
if (last) r.drawCircle(ray.B.x, ray.B.y, 8);
|
|
||||||
|
|
||||||
// Draw Label
|
|
||||||
if (label) {
|
|
||||||
label.text = text;
|
|
||||||
label.alpha = last ? 1.0 : 0.5;
|
|
||||||
label.visible = true;
|
|
||||||
let labelPosition = ray.project((ray.distance + 50) / ray.distance);
|
|
||||||
label.position.set(labelPosition.x, labelPosition.y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r.endFill();
|
|
||||||
}
|
|
||||||
|
|
||||||
_endMeasurement() {
|
|
||||||
super._endMeasurement();
|
|
||||||
this.draggedEntity = null;
|
|
||||||
if (this.pathfindingJob) {
|
|
||||||
routinglib.cancelPathfinding(this.pathfindingJob);
|
|
||||||
this.pathfindingJob = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The functions below aren't present in the orignal Ruler class and are added by Drag Ruler
|
|
||||||
dragRulerAddWaypoint(point, options = {}) {
|
|
||||||
options.snap = options.snap ?? true;
|
|
||||||
if (options.snap) {
|
|
||||||
point = getSnapPointForEntity(point.x, point.y, this.draggedEntity);
|
|
||||||
}
|
|
||||||
this.waypoints.push(new PIXI.Point(point.x, point.y));
|
|
||||||
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
|
|
||||||
this.waypoints
|
|
||||||
.filter(waypoint => waypoint.isPathfinding)
|
|
||||||
.forEach(waypoint => (waypoint.isPathfinding = false));
|
|
||||||
}
|
|
||||||
|
|
||||||
dragRulerAddWaypointHistory(waypoints) {
|
|
||||||
waypoints.forEach(waypoint => (waypoint.isPrevious = true));
|
|
||||||
this.waypoints = this.waypoints.concat(waypoints);
|
|
||||||
for (const waypoint of waypoints) {
|
|
||||||
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dragRulerClearWaypoints() {
|
|
||||||
this.waypoints = [];
|
|
||||||
this.labels.removeChildren().forEach(c => c.destroy());
|
|
||||||
}
|
|
||||||
|
|
||||||
dragRulerDeleteWaypoint(
|
|
||||||
event = {
|
|
||||||
preventDefault: () => {
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
options = {},
|
|
||||||
) {
|
|
||||||
this.dragRulerRemovePathfindingWaypoints();
|
|
||||||
options.snap = options.snap ?? true;
|
|
||||||
if (this.waypoints.filter(w => !w.isPrevious).length > 1) {
|
|
||||||
event.preventDefault();
|
|
||||||
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(
|
|
||||||
canvas.tokens,
|
|
||||||
);
|
|
||||||
const rulerOffset = this.rulerOffset;
|
|
||||||
|
|
||||||
// Options are not passed to _removeWaypoint in vanilla Foundry.
|
|
||||||
// Send them in case other modules have overriden that behavior and accept an options parameter (Toggle Snap to Grid)
|
|
||||||
this._removeWaypoint(
|
|
||||||
{x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y},
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
game.user.broadcastActivity({ruler: this});
|
|
||||||
} else {
|
|
||||||
this.dragRulerAbortDrag(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dragRulerRemovePathfindingWaypoints() {
|
|
||||||
this.waypoints
|
|
||||||
.filter(waypoint => waypoint.isPathfinding)
|
|
||||||
.forEach(_ =>
|
|
||||||
this.labels.removeChild(this.labels.children[this.labels.children.length - 1]).destroy(),
|
|
||||||
);
|
|
||||||
this.waypoints = this.waypoints.filter(waypoint => !waypoint.isPathfinding);
|
|
||||||
}
|
|
||||||
|
|
||||||
dragRulerAbortDrag(
|
|
||||||
event = {
|
|
||||||
preventDefault: () => {
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
const token = this.draggedEntity;
|
|
||||||
this._endMeasurement();
|
|
||||||
|
|
||||||
// Deactivate the drag workflow in mouse
|
|
||||||
token.mouseInteractionManager._deactivateDragEvents();
|
|
||||||
token.mouseInteractionManager.state = token.mouseInteractionManager.states.HOVER;
|
|
||||||
|
|
||||||
// This will cancel the current drag operation
|
|
||||||
// Pass in a fake event that hopefully is enough to allow other modules to function
|
|
||||||
token._onDragLeftCancel(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
async dragRulerRecalculate(tokenIds) {
|
|
||||||
if (this._state !== Ruler.STATES.MEASURING) return;
|
|
||||||
if (tokenIds && !tokenIds.includes(this.draggedEntity.id)) return;
|
|
||||||
const waypoints = this.waypoints.filter(waypoint => !waypoint.isPrevious);
|
|
||||||
this.dragRulerClearWaypoints();
|
|
||||||
if (game.settings.get(settingsKey, "enableMovementHistory"))
|
|
||||||
this.dragRulerAddWaypointHistory(getMovementHistory(this.draggedEntity));
|
|
||||||
for (const waypoint of waypoints) {
|
|
||||||
this.dragRulerAddWaypoint(waypoint, {snap: false});
|
|
||||||
}
|
|
||||||
this.measure(this.destination);
|
|
||||||
game.user.broadcastActivity({ruler: this});
|
|
||||||
}
|
|
||||||
|
|
||||||
static dragRulerGetRaysFromWaypoints(waypoints, destination) {
|
|
||||||
if (destination) waypoints = waypoints.concat([destination]);
|
|
||||||
return waypoints.slice(1).map((wp, i) => {
|
|
||||||
const ray = new Ray(waypoints[i], wp);
|
|
||||||
ray.isPrevious = Boolean(waypoints[i].isPrevious);
|
|
||||||
return ray;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
dragRulerGetColorForDistance(distance) {
|
|
||||||
if (!this.isDragRuler) return this.color;
|
|
||||||
if (!this.draggedEntity.actor) {
|
|
||||||
return this.color;
|
|
||||||
}
|
|
||||||
// Don't apply colors if the current user doesn't have at least observer permissions
|
|
||||||
if (this.draggedEntity.actor.permission < 2) {
|
|
||||||
// If this is a pc and alwaysShowSpeedForPCs is enabled we show the color anyway
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
this.draggedEntity.actor.data.type === "character" &&
|
|
||||||
game.settings.get(settingsKey, "alwaysShowSpeedForPCs")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return this.color;
|
|
||||||
}
|
|
||||||
distance = Math.round(distance * 100) / 100;
|
|
||||||
if (!this.dragRulerRanges)
|
|
||||||
this.dragRulerRanges = getRangesFromSpeedProvider(this.draggedEntity);
|
|
||||||
return (
|
|
||||||
getColorForDistanceAndToken(distance, this.draggedEntity, this.dragRulerRanges) ??
|
|
||||||
this.color
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
dragRulerStart(options, measureImmediately = true) {
|
|
||||||
const entity = this.draggedEntity;
|
|
||||||
const isToken = entity instanceof Token;
|
|
||||||
if (isToken && !currentSpeedProvider.usesRuler(entity)) return;
|
|
||||||
const ruler = canvas.controls.ruler;
|
|
||||||
ruler.clear();
|
|
||||||
ruler._state = Ruler.STATES.STARTING;
|
|
||||||
let entityCenter;
|
|
||||||
if (
|
|
||||||
isToken &&
|
|
||||||
canvas.grid.isHex &&
|
|
||||||
game.modules.get("hex-size-support")?.active &&
|
|
||||||
CONFIG.hexSizeSupport.getAltSnappingFlag(entity)
|
|
||||||
)
|
|
||||||
entityCenter = getHexSizeSupportTokenGridCenter(entity);
|
|
||||||
else entityCenter = entity.center;
|
|
||||||
if (isToken && game.settings.get(settingsKey, "enableMovementHistory"))
|
|
||||||
ruler.dragRulerAddWaypointHistory(getMovementHistory(entity));
|
|
||||||
ruler.dragRulerAddWaypoint(entityCenter, {snap: false});
|
|
||||||
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(
|
|
||||||
canvas.tokens,
|
|
||||||
);
|
|
||||||
const destination = {
|
|
||||||
x: mousePosition.x + ruler.rulerOffset.x,
|
|
||||||
y: mousePosition.y + ruler.rulerOffset.y,
|
|
||||||
};
|
|
||||||
if (measureImmediately) ruler.measure(destination, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
dragRulerSendState() {
|
|
||||||
game.user.broadcastActivity({
|
|
||||||
ruler: this.toJSON(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CONFIG.Canvas.rulerClass = DragRulerRuler;
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
export function getDefaultSpeedAttribute() {
|
|
||||||
switch (game.system.id) {
|
|
||||||
case "CoC7":
|
|
||||||
return "actor.system.attribs.mov.value";
|
|
||||||
case "dcc":
|
|
||||||
return "actor.system.attributes.speed.value";
|
|
||||||
case "dnd4e":
|
|
||||||
return "actor.system.movement.walk.value";
|
|
||||||
case "dnd5e":
|
|
||||||
return "actor.system.attributes.movement.walk";
|
|
||||||
case "lancer":
|
|
||||||
return "actor.system.derived.speed";
|
|
||||||
case "pf1":
|
|
||||||
case "D35E":
|
|
||||||
return "actor.system.attributes.speed.land.total";
|
|
||||||
case "sfrpg":
|
|
||||||
return "actor.system.attributes.speed.value";
|
|
||||||
case "shadowrun5e":
|
|
||||||
return "actor.system.movement.walk.value";
|
|
||||||
case "swade":
|
|
||||||
return "actor.system.stats.speed.adjusted";
|
|
||||||
case "ds4":
|
|
||||||
return "actor.system.combatValues.movement.total";
|
|
||||||
case "splittermond":
|
|
||||||
return "actor.derivedValues.speed.value";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDefaultDashMultiplier() {
|
|
||||||
switch (game.system.id) {
|
|
||||||
case "swade":
|
|
||||||
return 0;
|
|
||||||
case "dcc":
|
|
||||||
case "dnd4e":
|
|
||||||
case "dnd5e":
|
|
||||||
case "lancer":
|
|
||||||
case "pf1":
|
|
||||||
case "D35E":
|
|
||||||
case "sfrpg":
|
|
||||||
case "shadowrun5e":
|
|
||||||
case "ds4":
|
|
||||||
return 2;
|
|
||||||
case "CoC7":
|
|
||||||
return 5;
|
|
||||||
case "splittermond":
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user