Compare commits

...

78 Commits

Author SHA1 Message Date
Manuel Vögele 6027735f4c Release v1.13.0 2022-10-13 10:33:53 +02:00
Manuel Vögele 84bce39043 Add documentation for the pathfinding feature 2022-10-13 10:28:25 +02:00
Manuel Vögele 46c504341c Remove getCostForStep SpedProvider function in favor or the Enhanced Terrain Layer API 2022-10-13 08:18:59 +02:00
Manuel Vögele ff9b187d90 v10 compat: Use the new way of checking a wall collision 2022-10-13 08:07:08 +02:00
n0q bd76c965a2 Update README.md (#232) 2022-10-13 07:45:17 +02:00
Rapunzel77 ee9ac14346 adding TDE5 support to the README.md (#231)
Co-authored-by: Feiertag, Nico <nfeiertag@gmx.net>
2022-10-11 22:38:45 +02:00
Manuel Vögele 7a00d6fcc2 Stop pathfinding if the measurement is canceled mid-pathfinding 2022-10-04 21:32:26 +02:00
Manuel Vögele 4957188630 If a previous pathfinding job is done, reset it to undefined 2022-10-04 10:25:55 +02:00
Manuel Vögele 944ff96a26 Add comment about identical an identical function in another module 2022-10-03 10:51:09 +02:00
Manuel Vögele 58565b7011 v10 compat: Update to new token animation name 2022-10-01 07:56:20 +02:00
Manuel Vögele 25fc6258e7 Cancel running pathfinding jobs before scheduling a new one 2022-09-30 22:47:51 +02:00
Manuel Vögele ac00aac85b Gracefully handle situations where no path exists 2022-09-30 22:36:48 +02:00
Manuel Vögele 8d5a6716e1 v10 compat: Migrate manifest 2022-09-30 22:07:55 +02:00
Manuel Vögele c43b0bfd64 Rename js/ to src/ since the rust component is now gone 2022-09-30 22:04:41 +02:00
Manuel Vögele c21e4c91d6 Move pathfinding into routinglib; call routinglib for pathfinding jobs 2022-09-30 21:52:48 +02:00
Manuel Vögele 4b30d52da1 Fix several issues with Terrain Ruler based measurements 2022-09-30 19:46:25 +02:00
Manuel Vögele 08c39b5683 Fix references to the Ruler class 2022-09-30 14:08:38 +02:00
Manuel Vögele 2e2353e1d1 v10 compat: migrate to new data paths 2022-09-30 14:03:59 +02:00
Manuel Vögele cf74a11f18 v10 compat: Split measure function into several subfunctions as suggested by Foundry v10 2022-09-30 14:03:47 +02:00
Manuel Vögele a1c10e088d Add documentation to keybinding globals 2022-09-30 14:00:34 +02:00
Manuel Vögele 626829e989 v10 compat: migrate to new data paths 2022-09-28 08:07:02 +02:00
Manuel Vögele b87e91b63c v10 compat: Reference CONFIG.Canvas.rulerClass instead of Ruler 2022-09-28 08:05:14 +02:00
Manuel Vögele ed10144cc6 Disable background caching in preparation for routinglib 2022-09-28 07:46:34 +02:00
Manuel Vögele 5e82049686 Reformat with prettier 2022-09-28 07:35:16 +02:00
gsterling 71424cd284 add splittermond with speed and dash multiplier (#223) 2022-09-28 07:31:20 +02:00
Manuel Vögele 445c03d29a Add difficult terrain support for pathfinding on gridded scenes 2022-05-10 08:39:19 +02:00
Manuel Vögele 5904efbdd5 Remove redundant first waypoint from path generated by pathfinder
This fixes a bug where gridless snapping would snap slightly below the target range
2022-05-07 08:12:30 +02:00
Manuel Vögele d325f8acee Don't attempt to start background caching on gridless scenes, as it's currently unsupported (fixes #196) 2022-05-06 20:22:42 +02:00
Jonathan Calvert 1577435a33 Fix issue caused by client setting moving from Levels to Wall Height (#195) 2022-05-06 20:16:52 +02:00
Cole Schultz 1b8b2328c6 Add the pathfinding radius setting (#194) 2022-04-29 10:41:05 +02:00
Manuel Vögele 0dba39b145 Merge branch 'master' into develop 2022-04-12 00:22:20 +02:00
Manuel Vögele c1b0e9d7fb Release v1.12.8 2022-04-12 00:13:34 +02:00
Manuel Vögele 9d227efcd1 Enusre that measurements to determine the highlight color are always done center to center (fixes #189) 2022-04-12 00:11:30 +02:00
Manuel Vögele a3d942c287 Apply wall height's bounds to the rays before doing colission checks (fixes #190) 2022-04-12 00:11:30 +02:00
Manuel Vögele bdbd4fde18 Merge branch 'master' into develop 2022-04-01 18:07:25 +02:00
Manuel Vögele 3afb1f30b7 Release v1.12.7 2022-04-01 18:06:46 +02:00
Manuel Vögele 654f8e3111 Fix a typo that caused measurement template snapping to be broken (fixes #185) 2022-04-01 18:04:25 +02:00
Jonathan Calvert 2979e86201 Improve Pathfinding: Add background caching (#175) 2022-03-09 18:40:43 +01:00
Manuel Vögele 8f81fd707e Merge branch 'master' into develop 2022-03-08 23:13:36 +01:00
Manuel Vögele 5f6e348bf0 Release v1.12.6 2022-03-08 23:12:17 +01:00
Manuel Vögele 82f2a885fb getSnappedPosition cannot be used on gridless to calculate the snap point for measured templates 2022-03-08 23:09:06 +01:00
Manuel Vögele d536ae8dfb Merge branch 'master' into develop 2022-03-08 22:07:43 +01:00
Manuel Vögele 5add0be393 Release v1.12.5 2022-03-08 22:06:14 +01:00
Manuel Vögele f77990420a Replace getSnapPointForMeasuredTemplate with foundry native function (fixes #181) 2022-03-08 20:17:25 +01:00
Manuel Vögele 44efabc656 Merge branch 'master' into develop 2022-03-08 12:50:16 +01:00
Manuel Vögele e84632c0d7 Release v1.12.4 2022-03-08 12:49:56 +01:00
Manuel Vögele 30bd3d33e0 If no dragged entity is being transmitted, unset it (fixes #179) 2022-03-08 12:48:17 +01:00
Manuel Vögele 6604e322bd Show diagnoal count in debug print 2022-03-08 11:49:27 +01:00
Manuel Vögele b68f3bbe29 Elevation also needs to be respected if only "wall-height" is enabled 2022-03-08 11:45:58 +01:00
Manuel Vögele 793e87bc2f Merge branch 'master' into develop 2022-03-08 09:56:03 +01:00
Manuel Vögele df4c515fcd Release v1.12.3 2022-03-07 16:53:48 +01:00
Manuel Vögele 02dfd8db0d Only reclaculate the ruler if it's a Drag Ruler (fixes #161) 2022-03-07 14:39:31 +01:00
Manuel Vögele 65911c11f0 Only reclaculate the ruler if it's a Drag Ruler (fixes #161) 2022-03-07 14:37:15 +01:00
Manuel Vögele ddf89f9499 Add support for the wall height module in the gridless pathfinder 2022-03-07 12:40:16 +01:00
Jonathan Calvert 0dfdb23bfb Levels Compatibility: Clear pathfinding cache after changing elevation (#173) 2022-03-07 12:39:16 +01:00
Jonathan Calvert 3f2e9e1a3e Fix one-way wall collision detection (fixes #167) (#172) 2022-03-07 12:38:30 +01:00
Jonathan Calvert 846e39d946 Pathfinding: Keep Multiple Caches (#177) 2022-03-05 21:36:56 +01:00
Manuel Vögele 26748ee234 Revert ebd0ee6ccf, which introduced a bug instead of fixing one 2022-03-04 08:40:04 +01:00
Manuel Vögele ebd0ee6ccf Fix the order of colission checks during pathfinding waypoint culling to make one-way-walls work properly 2022-03-02 22:33:42 +01:00
Jonathan Calvert 1d477094b8 Fix bug with diagonal movement (#174) 2022-03-01 14:28:01 +01:00
Jonathan Calvert bbfce2bb77 Pathfinding Improvements: Improve 5-10-5 Diagonal Handling (#170) 2022-02-28 23:08:56 +01:00
Manuel Vögele 54cee52eb2 Add support for the wall height module in the gridless pathfinder 2022-02-28 22:48:15 +01:00
Jonathan Calvert a967a03197 Levels Compatibility: Clear pathfinding cache after changing elevation (#173) 2022-02-28 21:17:54 +01:00
Jonathan Calvert 8d243ce919 Fix one-way wall collision detection (fixes #167) (#172) 2022-02-28 19:13:12 +01:00
Jonathan Calvert b227d5073b Pathfinding Improvements: Replace nextNodes array with PriorityQueueSet (#169) 2022-02-28 18:23:46 +01:00
Jonathan Calvert 576db2dc5a Pathfinding Improvements: Bound Algorithm to Canvas (#168) 2022-02-28 09:52:12 +01:00
EndlesNights b031acaa6e dnd 4e system support (#162) 2022-02-28 09:42:10 +01:00
Manuel Vögele ee956abd78 Update .gitignore 2022-02-17 11:15:40 +01:00
Manuel Vögele 1334703877 Release v1.12.2 2022-02-17 00:58:33 +01:00
Manuel Vögele d6cccf4466 Ensure pathfinding on gridless is possible by always generating a pathfinding point when a virtual wall is being inserted 2022-02-17 00:53:51 +01:00
Manuel Vögele 074d2f5052 Disable pathfinding when the hotkey for moving tokens without pathfinding is enabled (fixes #158) 2022-02-17 00:47:13 +01:00
Manuel Vögele e5b8f896f8 Release v1.12.1 2022-02-16 12:48:59 +01:00
Manuel Vögele 3de350d3e9 Remove wasm snippets from release builds 2022-02-16 12:47:23 +01:00
Manuel Vögele 8f97081236 Release v1.12.0 2022-02-16 11:57:33 +01:00
Manuel Vögele 18253dc42b Delay reload to make sure all settings are being applied 2022-02-16 11:46:46 +01:00
Manuel Vögele fb702cd850 Add support for gridless pathfinding 2022-02-15 17:56:08 +01:00
Manuel Vögele ad3fdf4d18 Release v1.11.5 2022-02-12 18:29:52 +01:00
Manuel Vögele 58946096a6 Remove debugging output 2022-02-12 18:25:50 +01:00
23 changed files with 1497 additions and 1013 deletions
+5 -1
View File
@@ -1 +1,5 @@
foundry-*.js /foundry*.js
artifact/
wasm/
*.lock
.vscode/
+4
View File
@@ -0,0 +1,4 @@
printWidth: 100
trailingComma: "all"
bracketSpacing: false
arrowParens: "avoid"
+77
View File
@@ -1,3 +1,80 @@
## 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
### Bugfixes
- Fixed a bug that could cause grid cells to be highlighted in the wrong color
### Compatibility
- Fixed an interaction with the Wall Height module that could allow players to walk through walls
## 1.12.7
### Bugfixes
- Fixed a bug that caused measurement templates to only snap to the corners of the grid (this was a regression introduced in 1.12.5)
## 1.12.6
### Bugfixes
- Measured templates no longer snap to a virtual grid on gridless scenes (this was a regression introduced in 1.12.5)
## 1.12.5
### Compatibility
- Drag Ruler is now compatible with DF Template Enhancements
## 1.12.4
### Bugfixes
- Fixed a bug that could cause Drag Ruler to override the default ruler color on other player's clients
## 1.12.3
### Bugfixes
- Fixed a bug that could cause foundry to freeze indefinitely when trying to pathfind to an unreachalbe location (thanks to JDCalvert)
- Fixed a bug that caused the pathfinder to route through one-directional walls from the wrong direction (thanks to JDCalvert)
- Fixed a bug that could cause Drag Ruler to write errors into the JS console during regular usage
### Compatibility
- Drag Ruler's generic speed provider is now aware of good defaults for DnD 4th Edition
- Drag Ruler's pathfinder should now be compatible with the Wall Height and Levels modules (thanks to JDCalvert)
## 1.12.2
### Bugfixes
- Fixed a bug where the pathfinder on gridless scenes sometimes wasn't able to find a way around corners with specific angles
- Pathfinding will now be disabled when the hotkey to move tokens without animation is being pressed, to allow GMs to move their tokens through walls
## 1.12.1
### Hotfix
- Version 1.12.0 was incorrectly packaged, which caused it to fail to load
## 1.12.0
### New features
- Pathfinding is now supported on gridless scenes
## 1.11.5
### Bugfixes
- Fixed a bug that was causing Drag Ruler to spam useless warnings into the console (this was a regression introduced in 1.11.4)
## 1.11.4 ## 1.11.4
### Bugfixes ### Bugfixes
- When changing the measurement mode via a keybinding (toggle snaping or toggle pathfinding) the updated ruler will now be sent to other players immediately - When changing the measurement mode via a keybinding (toggle snaping or toggle pathfinding) the updated ruler will now be sent to other players immediately
+12
View File
@@ -32,6 +32,16 @@ During combat, Drag Ruler will remember the path a token has taken during it's t
![Demonstration of the Movement History](https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/709774b25f7dd818a90591165f74b3e6dbc788cc/media/movement_history.webp) ![Demonstration of the Movement History](https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/709774b25f7dd818a90591165f74b3e6dbc788cc/media/movement_history.webp)
## 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.
![Demonstration of pathfinding](https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/15cda2cdbe50d6c32ac75544edf43368560ab1bc/media/pathfinding.webp)
## 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.
@@ -39,6 +49,7 @@ 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))
@@ -52,6 +63,7 @@ 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)
+13 -8
View File
@@ -1,10 +1,12 @@
{ {
"name": "drag-ruler", "id": "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.11.4", "version": "1.13.0",
"minimumCoreVersion" : "9.245", "compatibility": {
"compatibleCoreVersion" : "9", "minimum": "10",
"verified": "10"
},
"authors": [ "authors": [
{ {
"name": "Manuel Vögele", "name": "Manuel Vögele",
@@ -57,15 +59,18 @@
"path": "lang/zh-tw.json" "path": "lang/zh-tw.json"
} }
], ],
"dependencies": [ "relationships": {
"requires": [
{ {
"name": "socketlib", "id": "socketlib",
"type": "module",
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-socketlib/master/module.json" "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.11.4.zip", "download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/v1.13.0.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",
+98 -81
View File
@@ -1,181 +1,198 @@
import {measureDistances} from "./compatibility.js"; import {measureDistances} from "./compatibility.js";
import {getMovementHistory} from "./movement_tracking.js"; import {getMovementHistory} from "./movement_tracking.js";
import {GenericSpeedProvider, SpeedProvider} from "./speed_provider.js" import {GenericSpeedProvider, SpeedProvider} from "./speed_provider.js";
import {settingsKey} from "./settings.js" import {settingsKey} from "./settings.js";
import {getTokenShape} from "./util.js"; import {getAreaFromPositionAndShape, getTokenShape} from "./util.js";
export const availableSpeedProviders = {} export const availableSpeedProviders = {};
export let currentSpeedProvider = undefined export let currentSpeedProvider = undefined;
function register(module, type, speedProvider) { function register(module, type, speedProvider) {
const id = `${type}.${module.id}` const id = `${type}.${module.id}`;
let providerInstance let providerInstance;
if (speedProvider.prototype instanceof SpeedProvider) { if (speedProvider.prototype instanceof SpeedProvider) {
providerInstance = new speedProvider(id) providerInstance = new speedProvider(id);
} } else {
else { console.warn(
console.warn(`Drag Ruler | The ${type} '${module.id}' uses the old, deprecated version of the Drag Ruler API. ` + `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. " + "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.`); `Please update the ${type} ${module.id} to stay compatible with future Drag Ruler versions.`,
speedProvider.id = id );
speedProvider.usesRuler = () => true speedProvider.id = id;
providerInstance = speedProvider speedProvider.usesRuler = () => true;
providerInstance = speedProvider;
} }
setupProvider(providerInstance) setupProvider(providerInstance);
} }
function setupProvider(speedProvider) { function setupProvider(speedProvider) {
if (speedProvider instanceof SpeedProvider) { if (speedProvider instanceof SpeedProvider) {
const unreachableColor = {id: "unreachable", default: speedProvider.defaultUnreachableColor, name: "drag-ruler.settings.speedProviderSettings.color.unreachable.name"} const unreachableColor = {
id: "unreachable",
default: speedProvider.defaultUnreachableColor,
name: "drag-ruler.settings.speedProviderSettings.color.unreachable.name",
};
for (const color of speedProvider.colors.concat([unreachableColor])) { for (const color of speedProvider.colors.concat([unreachableColor])) {
game.settings.register(settingsKey, `speedProviders.${speedProvider.id}.color.${color.id}`, { game.settings.register(settingsKey, `speedProviders.${speedProvider.id}.color.${color.id}`, {
config: false, config: false,
scope: "client", scope: "client",
type: Number, type: Number,
default: color.default, default: color.default,
}) });
} }
for (const setting of speedProvider.settings) { for (const setting of speedProvider.settings) {
setting.config = false setting.config = false;
game.settings.register(settingsKey, `speedProviders.${speedProvider.id}.setting.${setting.id}`, setting) game.settings.register(
settingsKey,
`speedProviders.${speedProvider.id}.setting.${setting.id}`,
setting,
);
} }
} }
availableSpeedProviders[speedProvider.id] = speedProvider availableSpeedProviders[speedProvider.id] = speedProvider;
game.settings.settings.get("drag-ruler.speedProvider").default = getDefaultSpeedProvider() game.settings.settings.get("drag-ruler.speedProvider").default = getDefaultSpeedProvider();
updateSpeedProvider() updateSpeedProvider();
} }
export function getDefaultSpeedProvider() { export function getDefaultSpeedProvider() {
const providerIds = Object.keys(availableSpeedProviders) const providerIds = Object.keys(availableSpeedProviders);
// Game systems take the highest precedence for the being the default // Game systems take the highest precedence for the being the default
const gameSystem = providerIds.find(key => key.startsWith("system.")) const gameSystem = providerIds.find(key => key.startsWith("system."));
if (gameSystem) if (gameSystem) return gameSystem;
return gameSystem
// If no game system is registered modules are next up. // 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 // 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 // settingKeys should always be sorted the same way so this should achive a stable default
const module = providerIds.find(key => key.startsWith("module.")) const module = providerIds.find(key => key.startsWith("module."));
if (module) if (module) return module;
return module
// If neither a game system or a module is found fall back to the native implementation // If neither a game system or a module is found fall back to the native implementation
return providerIds[0] return providerIds[0];
} }
export function updateSpeedProvider() { export function updateSpeedProvider() {
// If the configured provider is registered use that one. If not use the default provider // If the configured provider is registered use that one. If not use the default provider
const configuredProvider = game.settings.get("drag-ruler", "speedProvider") const configuredProvider = game.settings.get("drag-ruler", "speedProvider");
currentSpeedProvider = availableSpeedProviders[configuredProvider] ?? availableSpeedProviders[game.settings.settings.get("drag-ruler.speedProvider").default] currentSpeedProvider =
availableSpeedProviders[configuredProvider] ??
availableSpeedProviders[game.settings.settings.get("drag-ruler.speedProvider").default];
} }
export function initApi() { export function initApi() {
const genericSpeedProviderInstance = new GenericSpeedProvider("native") const genericSpeedProviderInstance = new GenericSpeedProvider("native");
setupProvider(genericSpeedProviderInstance) setupProvider(genericSpeedProviderInstance);
} }
export function getRangesFromSpeedProvider(token) { export function getRangesFromSpeedProvider(token) {
try { try {
if (currentSpeedProvider instanceof Function) if (currentSpeedProvider instanceof Function) return currentSpeedProvider(token, 0x00ff00);
return currentSpeedProvider(token, 0x00FF00) const ranges = currentSpeedProvider.getRanges(token);
const ranges = currentSpeedProvider.getRanges(token)
for (const range of ranges) { for (const range of ranges) {
range.color = game.settings.get(settingsKey, `speedProviders.${currentSpeedProvider.id}.color.${range.color}`) range.color = game.settings.get(
settingsKey,
`speedProviders.${currentSpeedProvider.id}.color.${range.color}`,
);
} }
return ranges return ranges;
} } catch (e) {
catch (e) { console.error(e);
console.error(e) return [];
return []
} }
} }
export function getUnreachableColorFromSpeedProvider() { export function getUnreachableColorFromSpeedProvider() {
if (currentSpeedProvider instanceof Function) if (currentSpeedProvider instanceof Function) return 0xff0000;
return 0xFF0000
try { try {
return game.settings.get(settingsKey, `speedProviders.${currentSpeedProvider.id}.color.unreachable`) return game.settings.get(
} settingsKey,
catch (e) { `speedProviders.${currentSpeedProvider.id}.color.unreachable`,
console.error(e) );
return 0xFF0000 } catch (e) {
}
}
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); console.error(e);
return 1; return 0xff0000;
} }
} }
export function getColorForDistanceAndToken(distance, token, ranges=null) { export function getColorForDistanceAndToken(distance, token, ranges = null) {
if (!ranges) { if (!ranges) {
ranges = getRangesFromSpeedProvider(token); ranges = getRangesFromSpeedProvider(token);
} }
if (ranges.length === 0) if (ranges.length === 0) return null;
return null; const currentRange = ranges.reduce(
const currentRange = ranges.reduce((minRange, currentRange) => { (minRange, currentRange) => {
if (distance <= currentRange.range && currentRange.range < minRange.range) if (distance <= currentRange.range && currentRange.range < minRange.range)
return currentRange; return currentRange;
return minRange; return minRange;
}, {range: Infinity, color: getUnreachableColorFromSpeedProvider()}); },
{range: Infinity, color: getUnreachableColorFromSpeedProvider()},
);
return currentRange.color; return currentRange.color;
} }
export function getMovedDistanceFromToken(token) { export function getMovedDistanceFromToken(token) {
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active; const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active;
const history = getMovementHistory(token); const history = getMovementHistory(token);
const segments = Ruler.dragRulerGetRaysFromWaypoints(history, {x: token.x, y: token.y}).map(ray => {return {ray}}); const segments = CONFIG.Canvas.rulerClass
.dragRulerGetRaysFromWaypoints(history, {x: token.x, y: token.y})
.map(ray => {
return {ray};
});
const shape = getTokenShape(token); const shape = getTokenShape(token);
const distances = measureDistances(segments, token, shape, {enableTerrainRuler: terrainRulerAvailable}); const distances = measureDistances(segments, token, shape, {
enableTerrainRuler: terrainRulerAvailable,
});
// Sum up the distances // Sum up the distances
return distances.reduce((acc, val) => acc + val, 0); return distances.reduce((acc, val) => acc + val, 0);
} }
export function buildCostFunction(token, shape) {
return (x, y, costOptions = {}) => {
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) { export function registerModule(moduleId, speedProvider) {
// Check if a module with the given id exists and is currently enabled // Check if a module with the given id exists and is currently enabled
const module = game.modules.get(moduleId) const module = game.modules.get(moduleId);
// If it doesn't the calling module did something wrong. Log a warning and ignore this module // If it doesn't the calling module did something wrong. Log a warning and ignore this module
if (!module) { if (!module) {
console.warn( console.warn(
`Drag Ruler | A module tried to register with the id "${moduleId}". However no active module with this id was found.` + `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. " + "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 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.") "If this call was made form a game system instead of a module please use `registerSystem` instead.",
return );
return;
} }
// Using Drag Ruler's id is not allowed // Using Drag Ruler's id is not allowed
if (moduleId === "drag-ruler") { if (moduleId === "drag-ruler") {
console.warn( console.warn(
`Drag Ruler | A module tried to register with the id "${moduleId}", which is not allowed. This api registration call was ignored. ` + `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 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." "If this call was made form a game system instead of a module please use `registerSystem` instead.",
) );
return return;
} }
register(module, "module", speedProvider) register(module, "module", speedProvider);
} }
export function registerSystem(systemId, speedProvider) { export function registerSystem(systemId, speedProvider) {
const system = game.system 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 the current system id doesn't match the provided id something went wrong. Log a warning and ignore this module
if (system.id != systemId) { if (system.id != systemId) {
console.warn( console.warn(
`Drag Ruler | A system tried to register with the id "${systemId}". However the active system has a different id.` + `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. " + "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 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.") "If this call was made form a module instead of a game system please use `registerModule` instead.",
return );
return;
} }
register(system, "system", speedProvider) register(system, "system", speedProvider);
} }
+50 -26
View File
@@ -1,21 +1,26 @@
import {getCostFromSpeedProvider} from "./api.js"; import {buildCostFunction} from "./api.js";
import {settingsKey} from "./settings.js"; import {settingsKey} from "./settings.js";
import {getAreaFromPositionAndShape, highlightTokenShape} from "./util.js"; import {highlightTokenShape} from "./util.js";
export function getHexSizeSupportTokenGridCenter(token) { export function getHexSizeSupportTokenGridCenter(token) {
const tokenCenterOffset = CONFIG.hexSizeSupport.getCenterOffset(token) const tokenCenterOffset = CONFIG.hexSizeSupport.getCenterOffset(token);
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(ray, startDistance, tokenShape=[{x: 0, y: 0}], alpha=1) { export function highlightMeasurementTerrainRuler(
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;
@@ -25,17 +30,23 @@ 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(segment => segment.ray.dragRulerVisitedSpaces[segment.ray.dragRulerVisitedSpaces.length - 1].distance); const distances = previousSegments.map(
previousSegments.forEach(segment => segment.ray.terrainRulerVisitedSpaces = duplicate(segment.ray.dragRulerVisitedSpaces)); segment =>
opts.costFunction = (x, y, costOptions={}) => { return getCostFromSpeedProvider(entity, getAreaFromPositionAndShape({x, y}, shape), costOptions); } segment.ray.dragRulerVisitedSpaces[segment.ray.dragRulerVisitedSpaces.length - 1].distance,
);
previousSegments.forEach(
segment =>
(segment.ray.terrainRulerVisitedSpaces = duplicate(segment.ray.dragRulerVisitedSpaces)),
);
opts.costFunction = buildCostFunction(entity, shape);
if (previousSegments.length > 0) if (previousSegments.length > 0)
opts.terrainRulerInitialState = previousSegments[previousSegments.length - 1].ray.dragRulerFinalState; opts.terrainRulerInitialState =
previousSegments[previousSegments.length - 1].ray.dragRulerFinalState;
return distances.concat(terrainRuler.measureDistances(newSegments, opts)); return distances.concat(terrainRuler.measureDistances(newSegments, opts));
} } 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);
@@ -44,23 +55,31 @@ 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("Drag Ruler | The `socketlib` module isn't enabled, but it's required for Drag Ruler to operate properly."); console.error(
"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("drag-ruler.dependencies.socketlib.title")}</h2><p>${game.i18n.localize("drag-ruler.dependencies.socketlib.text")}</p>`, content: `<h2>${game.i18n.localize(
"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 (
else if (!game.modules.get("terrain-ruler")?.active && game.user.isGM && !game.settings.get(settingsKey, "neverShowTerrainRulerHint")) { !game.modules.get("terrain-ruler")?.active &&
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) { // One week if (Date.now() - lastHint > 604800000) {
// 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;
@@ -68,20 +87,25 @@ 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("drag-ruler.dependencies.terrain-ruler.title")}</h2><p>${game.i18n.format("drag-ruler.dependencies.terrain-ruler.text", {moduleName: enabledTerrainModule})}</p>`, 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>`,
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: () => game.settings.set(settingsKey, "lastTerrainRulerHintTime", Date.now()), callback: () =>
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);
} }
} }
+138
View File
@@ -0,0 +1,138 @@
/**
* A combination queue/set where the elements are ordered (in ascending order, according to the given priority function)
* and unique (according to the given elementMatcher).
*
* If an element is added to the set and an equivalent element already exists, the lower-priority one is discarded.
*/
export class PriorityQueueSet {
constructor(elementMatcher, priorityFunction) {
this.first = null;
this.elementMatcher = elementMatcher;
this.priorityFunction = priorityFunction;
}
pushWithPriority(value) {
const newNode = {value, priority: this.priorityFunction(value), next: null};
// If the queue is currently empty, we can just set this new node as the first and we're done
if (!this.first) {
this.first = newNode;
return;
}
let inserted = false;
let previous;
let current = this.first;
// Loop through the existing elements
while (current) {
if (this.elementMatcher(current.value, value)) {
// We've found an equivalent element before one with a lower priority. This one has at least
// the same priority as the new one, so don't bother inserting
return;
} else if (newNode.priority <= current.priority) {
// We've found some element with lower priority than the new one, so insert the new one just before it
newNode.next = current;
if (previous) {
previous.next = newNode;
} else {
this.first = newNode;
}
inserted = true;
previous = current;
current = current.next;
break;
}
previous = current;
current = current.next;
}
if (inserted) {
// Go through the rest of the list and try to find an equivalent element to the new one.
// We know it has higher priority than the new one, so remove it.
while (current) {
if (this.elementMatcher(current.value, value)) {
if (previous) {
previous.next = current.next;
} else {
this.first = current.next;
}
return;
}
previous = current;
current = current.next;
}
} else {
// We reached the end of the queue without finding a lower-priority or existing element, so
// insert the new one at the end
previous.next = newNode;
}
}
hasNext() {
return !!this.first;
}
pop() {
const first = this.first;
this.first = first?.next;
return first?.value;
}
}
/**
* Queue that will only ever accept elements with a given value once. Elements must have a "value" field, the
* JSON representation of which will be used as the key to match
*/
export class ProcessOnceQueue {
constructor() {
this.first = null;
this.last = null;
this.previouslyQueued = new Set();
}
/**
* Remove everything from the queue and forget all the previously-queued items
*/
reset() {
this.first = null;
this.last = null;
this.previouslyQueued.clear();
}
push(element) {
if (this.previouslyQueued.has(element)) {
return;
}
this.previouslyQueued.add(element);
const newNode = {
value: element,
next: null,
previous: null,
};
if (!this.first) {
this.first = newNode;
this.last = newNode;
} else {
this.last.next = newNode;
newNode.previous = this.last;
this.last = newNode;
}
}
pop() {
const node = this.first;
this.first = node?.next;
if (!node?.next) {
this.last = null;
}
return node?.value;
}
hasNext() {
return !!this.first;
}
}
+5 -6
View File
@@ -3,18 +3,17 @@
// 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) if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS) return [y, x];
return [y, x] return [x, y];
return [x, y]
} }
export function getGridPositionFromPixelsObj(o) { export function getGridPositionFromPixelsObj(o) {
+76 -210
View File
@@ -1,12 +1,8 @@
import {highlightMeasurementTerrainRuler, measureDistances} from "./compatibility.js"; import {getGridPositionFromPixels} from "./foundry_fixes.js";
import {getGridPositionFromPixels, getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
import {Line} from "./geometry.js";
import {disableSnap, moveWithoutAnimation} from "./keybindings.js"; import {disableSnap, moveWithoutAnimation} from "./keybindings.js";
import {trackRays} from "./movement_tracking.js" import {trackRays} from "./movement_tracking.js";
import {findPath, isPathfindingEnabled} from "./pathfinding.js";
import {settingsKey} from "./settings.js";
import {recalculate} from "./socket.js"; import {recalculate} from "./socket.js";
import {applyTokenSizeOffset, enumeratedZip, getSnapPointForEntity, getSnapPointForToken, getSnapPointForTokenObj, getTokenShape, highlightTokenShape, sum} from "./util.js"; import {getSnapPointForToken, highlightTokenShape, sum} from "./util.js";
// This is a modified version of Ruler.moveToken from foundry 0.7.9 // This is a modified version of Ruler.moveToken from foundry 0.7.9
export async function moveEntities(draggedEntity, selectedEntities) { export async function moveEntities(draggedEntity, selectedEntities) {
@@ -27,9 +23,17 @@ export async function moveEntities(draggedEntity, selectedEntities) {
if (!game.user.isGM && draggedEntity instanceof Token) { if (!game.user.isGM && draggedEntity instanceof Token) {
const hasCollision = selectedEntities.some(token => { const hasCollision = selectedEntities.some(token => {
const offset = calculateEntityOffset(token, draggedEntity); const offset = calculateEntityOffset(token, draggedEntity);
const offsetRays = rays.filter(ray => !ray.isPrevious).map(ray => applyOffsetToRay(ray, offset)) const offsetRays = rays
return offsetRays.some(r => canvas.walls.checkCollision(r)); .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) { if (hasCollision) {
ui.notifications.error(game.i18n.localize("ERROR.TokenCollide")); ui.notifications.error(game.i18n.localize("ERROR.TokenCollide"));
this._endMeasurement(); this._endMeasurement();
@@ -43,8 +47,7 @@ export async function moveEntities(draggedEntity, selectedEntities) {
await animateEntities.call(this, selectedEntities, draggedEntity, rays, wasPaused); await animateEntities.call(this, selectedEntities, draggedEntity, rays, wasPaused);
// Once all animations are complete we can clear the ruler // Once all animations are complete we can clear the ruler
if (this.draggedEntity?.id === draggedEntity.id) if (this.draggedEntity?.id === draggedEntity.id) this._endMeasurement();
this._endMeasurement();
} }
// This is a modified version code extracted from Ruler.moveToken from foundry 0.7.9 // This is a modified version code extracted from Ruler.moveToken from foundry 0.7.9
@@ -60,12 +63,11 @@ async function animateEntities(entities, draggedEntity, draggedRays, wasPaused)
const origin = [firstWaypoint.x + entityOffset.x, firstWaypoint.y + entityOffset.y]; const origin = [firstWaypoint.x + entityOffset.x, firstWaypoint.y + entityOffset.y];
let dx, dy; let dx, dy;
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) { if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
dx = entity.data.x - origin[0]; dx = entity.x - origin[0];
dy = entity.data.y - origin[1]; dy = entity.y - origin[1];
} } else {
else { dx = entity.x - origin[0];
dx = entity.data.x - origin[0]; dy = entity.y - origin[1];
dy = entity.data.y - origin[1];
} }
return {entity, rays: offsetRays, dx, dy}; return {entity, rays: offsetRays, dx, dy};
@@ -78,7 +80,7 @@ async function animateEntities(entities, draggedEntity, draggedRays, wasPaused)
// This is a flag of the "Monk's Active Tile Triggers" module that signals that the movement should be cancelled early // This is a flag of the "Monk's Active Tile Triggers" module that signals that the movement should be cancelled early
this.cancelMovement = false; this.cancelMovement = false;
for (let i = startWaypoint;i < entityAnimationData[0].rays.length; i++) { for (let i = startWaypoint; i < entityAnimationData[0].rays.length; i++) {
if (!wasPaused && game.paused) break; if (!wasPaused && game.paused) break;
const entityPaths = entityAnimationData.map(({entity, rays, dx, dy}) => { const entityPaths = entityAnimationData.map(({entity, rays, dx, dy}) => {
const ray = rays[i]; const ray = rays[i];
@@ -89,26 +91,38 @@ async function animateEntities(entities, draggedEntity, draggedRays, wasPaused)
const updates = entityPaths.map(({entity, path}) => { const updates = entityPaths.map(({entity, path}) => {
return {x: path.B.x, y: path.B.y, _id: entity.id}; return {x: path.B.x, y: path.B.y, _id: entity.id};
}); });
await draggedEntity.scene.updateEmbeddedDocuments(draggedEntity.constructor.embeddedName, updates, {animate}); await draggedEntity.scene.updateEmbeddedDocuments(
draggedEntity.constructor.embeddedName,
updates,
{animate},
);
if (animate) if (animate)
await Promise.all(entityPaths.map(({entity}) => CanvasAnimation.getAnimation(entity.movementAnimationName)?.promise)); 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 // This is a flag of the "Monk's Active Tile Triggers" module that signals that the movement should be cancelled early
if (this.cancelMovement) { if (this.cancelMovement) {
entityAnimationData.forEach(ead => ead.rays = ead.rays.slice(0, i + 1)); entityAnimationData.forEach(ead => (ead.rays = ead.rays.slice(0, i + 1)));
break; break;
} }
} }
if (isToken) if (isToken)
trackRays(entities, entityAnimationData.map(({rays}) => rays)).then(() => recalculate(entities)); trackRays(
entities,
entityAnimationData.map(({rays}) => rays),
).then(() => recalculate(entities));
} }
function calculateEntityOffset(entityA, entityB) { function calculateEntityOffset(entityA, entityB) {
return {x: entityA.data.x - entityB.data.x, y: entityA.data.y - entityB.data.y}; return {x: entityA.x - entityB.x, y: entityA.y - entityB.y};
} }
function applyOffsetToRay(ray, offset) { 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}); 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; newRay.isPrevious = ray.isPrevious;
return newRay; return newRay;
} }
@@ -118,7 +132,10 @@ export function onMouseMove(event) {
if (this._state === Ruler.STATES.MOVING) return; if (this._state === Ruler.STATES.MOVING) return;
// Extract event data // Extract event data
const destination = {x: event.data.destination.x + this.rulerOffset.x, y: event.data.destination.y + this.rulerOffset.y} const destination = {
x: event.data.destination.x + this.rulerOffset.x,
y: event.data.destination.y + this.rulerOffset.y,
};
// Hide any existing Token HUD // Hide any existing Token HUD
canvas.hud.token.clear(); canvas.hud.token.clear();
@@ -137,12 +154,21 @@ function scheduleMeasurement(destination, event) {
event._measureTime = Date.now(); event._measureTime = Date.now();
this._state = Ruler.STATES.MEASURING; this._state = Ruler.STATES.MEASURING;
cancelScheduledMeasurement.call(this); cancelScheduledMeasurement.call(this);
} } else {
else {
this.deferredMeasurementData = {destination, event}; this.deferredMeasurementData = {destination, event};
if (!this.deferredMeasurementTimeout) { if (!this.deferredMeasurementTimeout) {
this.deferredMeasurementPromise = new Promise((resolve, reject) => this.deferredMeasurementResolve = resolve); this.deferredMeasurementPromise = new Promise(
this.deferredMeasurementTimeout = window.setTimeout(() => scheduleMeasurement.call(this, this.deferredMeasurementData.destination, this.deferredMeasurementData.event), measurementInterval); (resolve, reject) => (this.deferredMeasurementResolve = resolve),
);
this.deferredMeasurementTimeout = window.setTimeout(
() =>
scheduleMeasurement.call(
this,
this.deferredMeasurementData.destination,
this.deferredMeasurementData.event,
),
measurementInterval,
);
} }
} }
} }
@@ -153,195 +179,34 @@ export function cancelScheduledMeasurement() {
this.deferredMeasurementResolve?.(); this.deferredMeasurementResolve?.();
} }
// This is a modified version of Ruler.measure form foundry 0.7.9 export function highlightMeasurementNative(
export function measure(destination, options={}) { ray,
previousSegments,
const isToken = this.draggedEntity instanceof Token; tokenShape = [{x: 0, y: 0}],
if (isToken && !this.draggedEntity.isVisible) alpha = 1,
return [] ) {
const spacer = canvas.scene.gridType === CONST.GRID_TYPES.SQUARE ? 1.41 : 1;
options.snap = options.snap ?? !disableSnap; const nMax = Math.max(
Math.floor(ray.distance / (spacer * Math.min(canvas.grid.w, canvas.grid.h))),
if (options.snap) { 1,
destination = getSnapPointForEntity(destination.x, destination.y, this.draggedEntity); );
} const tMax = Array.fromRange(nMax + 1).map(t => t / nMax);
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 = 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 // Track prior position
let prior = null; let prior = null;
// Iterate over ray portions // Iterate over ray portions
for ( let [i, t] of tMax.reverse().entries() ) { for (let [i, t] of tMax.reverse().entries()) {
let {x, y} = ray.project(t); let {x, y} = ray.project(t);
// Get grid position // Get grid position
let [x0, y0] = (i === 0) ? [null, null] : prior; let [x0, y0] = i === 0 ? [null, null] : prior;
let [x1, y1] = canvas.grid.grid.getGridPositionFromPixels(x, y); let [x1, y1] = canvas.grid.grid.getGridPositionFromPixels(x, y);
if ( x0 === x1 && y0 === y1 ) continue; if (x0 === x1 && y0 === y1) continue;
// Highlight the grid position // Highlight the grid position
let [xg, yg] = canvas.grid.grid.getPixelsFromGridPosition(x1, y1); 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 pathUntilSpace = previousSegments.concat([{ray: new Ray(ray.A, {x: xg, y: yg})}]);
const distance = sum(canvas.grid.measureDistances(pathUntilSpace, {gridSpaces: true})); const distance = sum(canvas.grid.measureDistances(pathUntilSpace, {gridSpaces: true}));
const color = this.dragRulerGetColorForDistance(distance); const color = this.dragRulerGetColorForDistance(distance);
@@ -352,10 +217,11 @@ export function highlightMeasurementNative(ray, previousSegments, tokenShape=[{x
// If the positions are not neighbors, also highlight their halfway point // If the positions are not neighbors, also highlight their halfway point
if (i > 0 && !canvas.grid.isNeighbor(x0, y0, x1, y1)) { if (i > 0 && !canvas.grid.isNeighbor(x0, y0, x1, y1)) {
let th = tMax[i - 1] - (0.5 / nMax); let th = tMax[i - 1] - 0.5 / nMax;
let {x, y} = ray.project(th); let {x, y} = ray.project(th);
let [x1h, y1h] = canvas.grid.grid.getGridPositionFromPixels(x, y); let [x1h, y1h] = canvas.grid.grid.getGridPositionFromPixels(x, y);
let [xgh, ygh] = canvas.grid.grid.getPixelsFromGridPosition(x1h, y1h); 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 pathUntilSpace = previousSegments.concat([{ray: new Ray(ray.A, {x: xgh, y: ygh})}]);
const distance = sum(canvas.grid.measureDistances(pathUntilSpace, {gridSpaces: true})); const distance = sum(canvas.grid.measureDistances(pathUntilSpace, {gridSpaces: true}));
const color = this.dragRulerGetColorForDistance(distance); const color = this.dragRulerGetColorForDistance(distance);
+90
View File
@@ -0,0 +1,90 @@
/**
* Functions taken directly from the hex-size-support module (https://github.com/Ourobor/Hex-Size-Support/releases/tag/1.1.0).
* Unless otherwise stated, these functions are taken as-is.
*/
/**
* Altered version of this function.
* - Instead of taking a token as a parameter to retrieve the altOrientationFlag, receive the flag value directly
* - Instead of taking a grid parameter, get the grid value from the globas canvas
*/
export function findVertexSnapPoint(x, y, altOrientationFlag) {
const grid = canvas.grid.grid;
if (grid.columns) {
return findSnapPointCols(x, y, grid.h, grid.w, altOrientationFlag);
} else {
return findSnapPointRows(x, y, grid.h, grid.w, altOrientationFlag);
}
}
function findSnapPointRows(x, y, h, w, alt) {
let xOffset = 0.0;
if (canvas.grid.grid.even) {
xOffset = -0.5;
}
let yOffset1 = 0.75;
let yOffset2 = 0.0;
if (alt) {
yOffset1 = 0.25;
yOffset2 = 1.0;
}
let row1 = calculateSnapPointsRows(x, y, h, w, 0.5 + xOffset, yOffset1);
let row2 = calculateSnapPointsRows(x, y, h, w, 1.0 + xOffset, yOffset2);
let dist1 = Math.pow(row1.x - x, 2) + Math.pow(row1.y - y, 2);
let dist2 = Math.pow(row2.x - x, 2) + Math.pow(row2.y - y, 2);
if (dist1 < dist2) {
return row1;
} else {
return row2;
}
}
function calculateSnapPointsRows(x, y, h, w, xOff, yOff) {
let c = Math.floor((x + (0.5 - xOff) * w) / w + 1);
let r = Math.floor((y + (0.75 - yOff) * h) / (1.5 * h) + 1);
let snapX = c * w - (1 - xOff) * w;
let snapY = r * h * 1.5 - (1.5 - yOff) * h;
return {x: snapX, y: snapY};
}
function findSnapPointCols(x, y, h, w, alt) {
let yOffset = 0.0;
if (canvas.grid.grid.even) {
yOffset = -0.5;
}
let xOffset1 = 0.25;
let xOffset2 = 1.0;
if (alt) {
xOffset1 = 0.75;
xOffset2 = 0.0;
}
let row1 = calculateSnapPointsCols(x, y, h, w, xOffset1, 0.5 + yOffset);
let row2 = calculateSnapPointsCols(x, y, h, w, xOffset2, 1.0 + yOffset);
let dist1 = Math.pow(row1.x - x, 2) + Math.pow(row1.y - y, 2);
let dist2 = Math.pow(row2.x - x, 2) + Math.pow(row2.y - y, 2);
if (dist1 < dist2) {
return row1;
} else {
return row2;
}
}
function calculateSnapPointsCols(x, y, h, w, xOff, yOff) {
let c = Math.floor((x + (0.75 - xOff) * w) / (1.5 * w) + 1);
let r = Math.floor((y + (0.5 - yOff) * h) / h + 1);
let snapX = c * w * 1.5 - (1.5 - xOff) * w;
let snapY = r * h - (1 - yOff) * h;
return {x: snapX, y: snapY};
}
+40 -25
View File
@@ -1,26 +1,35 @@
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,
}); });
@@ -35,9 +44,11 @@ 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,
}); });
@@ -46,12 +57,15 @@ 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",
@@ -60,12 +74,12 @@ export function registerKeybindings() {
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) if (!ruler?.draggedEntity) return false;
return false;
ruler.dragRulerDeleteWaypoint(); ruler.dragRulerDeleteWaypoint();
return true; return true;
} }
@@ -74,16 +88,14 @@ 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) if (!ruler?.draggedEntity) return false;
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;
@@ -91,8 +103,7 @@ function handleCreateWaypoint() {
function cancelDrag() { function cancelDrag() {
const ruler = canvas.controls.ruler; const ruler = canvas.controls.ruler;
if (!ruler?.draggedEntity) if (!ruler?.draggedEntity) return false;
return false;
ruler.dragRulerAbortDrag(); ruler.dragRulerAbortDrag();
return true; return true;
} }
@@ -101,10 +112,8 @@ function handleDisableSnap(event) {
disableSnap = !event.up; disableSnap = !event.up;
const ruler = canvas.controls.ruler; const ruler = canvas.controls.ruler;
if (!ruler?.isDragRuler) if (!ruler?.isDragRuler) return false;
return false; if (ruler._state !== Ruler.STATES.MEASURING) return false;
if (ruler._state !== Ruler.STATES.MEASURING)
return false;
ruler.measure(getMeasurePosition(), {snap: !disableSnap}); ruler.measure(getMeasurePosition(), {snap: !disableSnap});
ruler.dragRulerSendState(); ruler.dragRulerSendState();
@@ -113,16 +122,22 @@ function handleDisableSnap(event) {
function handleMoveWithoutAnimation(event) { function handleMoveWithoutAnimation(event) {
moveWithoutAnimation = !event.up; moveWithoutAnimation = !event.up;
const ruler = canvas.controls.ruler;
if (!ruler?.isDragRuler) return false;
if (ruler._state !== Ruler.STATES.MEASURING) return false;
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
ruler.dragRulerSendState();
return false;
} }
function handleTogglePathfinding(event) { function handleTogglePathfinding(event) {
togglePathfinding = !event.up; togglePathfinding = !event.up;
const ruler = canvas.controls.ruler; const ruler = canvas.controls.ruler;
if (!ruler?.isDragRuler) if (!ruler?.isDragRuler) return false;
return false; if (ruler._state !== Ruler.STATES.MEASURING) return false;
if (ruler._state !== Ruler.STATES.MEASURING)
return false;
ruler.measure(getMeasurePosition(), {snap: !disableSnap}); ruler.measure(getMeasurePosition(), {snap: !disableSnap});
ruler.dragRulerSendState(); ruler.dragRulerSendState();
+48 -31
View File
@@ -1,73 +1,90 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2021 fvtt-lib-wrapper Rui Pinheiro // Copyright © 2021 fvtt-lib-wrapper Rui Pinheiro
"use strict";
'use strict';
// A shim for the libWrapper library // A shim for the libWrapper library
export let libWrapper = undefined; export let libWrapper = undefined;
export const VERSIONS = [1,11,0]; export const VERSIONS = [1, 11, 0];
export const TGT_SPLIT_RE = new RegExp("([^.[]+|\\[('([^'\\\\]|\\\\.)+?'|\"([^\"\\\\]|\\\\.)+?\")\\])", 'g'); export const TGT_SPLIT_RE = new RegExp(
export const TGT_CLEANUP_RE = new RegExp("(^\\['|'\\]$|^\\[\"|\"\\]$)", 'g'); "([^.[]+|\\[('([^'\\\\]|\\\\.)+?'|\"([^\"\\\\]|\\\\.)+?\")\\])",
"g",
);
export const TGT_CLEANUP_RE = new RegExp("(^\\['|'\\]$|^\\[\"|\"\\]$)", "g");
// Main shim code // Main shim code
Hooks.once('init', () => { Hooks.once("init", () => {
// Check if the real module is already loaded - if so, use it // Check if the real module is already loaded - if so, use it
if(globalThis.libWrapper && !(globalThis.libWrapper.is_fallback ?? true)) { if (globalThis.libWrapper && !(globalThis.libWrapper.is_fallback ?? true)) {
libWrapper = globalThis.libWrapper; libWrapper = globalThis.libWrapper;
return; return;
} }
// Fallback implementation // Fallback implementation
libWrapper = class { libWrapper = class {
static get is_fallback() { return true }; static get is_fallback() {
return true;
}
static get WRAPPER() { return 'WRAPPER' }; static get WRAPPER() {
static get MIXED() { return 'MIXED' }; return "WRAPPER";
static get OVERRIDE() { return 'OVERRIDE' }; }
static get MIXED() {
return "MIXED";
}
static get OVERRIDE() {
return "OVERRIDE";
}
static register(package_id, target, fn, type="MIXED", {chain=undefined}={}) { static register(package_id, target, fn, type = "MIXED", {chain = undefined} = {}) {
const is_setter = target.endsWith('#set'); const is_setter = target.endsWith("#set");
target = !is_setter ? target : target.slice(0, -4); 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 split = target
const root_nm = split.splice(0,1)[0]; .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; let obj, fn_name;
if(split.length == 0) { if (split.length == 0) {
obj = globalThis; obj = globalThis;
fn_name = root_nm; fn_name = root_nm;
} } else {
else {
const _eval = eval; const _eval = eval;
fn_name = split.pop(); fn_name = split.pop();
obj = split.reduce((x,y)=>x[y], globalThis[root_nm] ?? _eval(root_nm)); obj = split.reduce((x, y) => x[y], globalThis[root_nm] ?? _eval(root_nm));
} }
let iObj = obj; let iObj = obj;
let descriptor = null; let descriptor = null;
while(iObj) { while (iObj) {
descriptor = Object.getOwnPropertyDescriptor(iObj, fn_name); descriptor = Object.getOwnPropertyDescriptor(iObj, fn_name);
if(descriptor) break; if (descriptor) break;
iObj = Object.getPrototypeOf(iObj); 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.`; 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; 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); }; 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 (!is_setter) {
if(descriptor.value) { if (descriptor.value) {
original = descriptor.value; original = descriptor.value;
descriptor.value = wrapper; descriptor.value = wrapper;
} } else {
else {
original = descriptor.get; original = descriptor.get;
descriptor.get = wrapper; descriptor.get = wrapper;
} }
} } else {
else { if (!descriptor.set) throw `libWrapper Shim: '${target}' does not have a setter`;
if(!descriptor.set) throw `libWrapper Shim: '${target}' does not have a setter`;
original = descriptor.set; original = descriptor.set;
descriptor.set = wrapper; descriptor.set = wrapper;
} }
@@ -75,5 +92,5 @@ Hooks.once('init', () => {
descriptor.configurable = true; descriptor.configurable = true;
Object.defineProperty(obj, fn_name, descriptor); Object.defineProperty(obj, fn_name, descriptor);
} }
} };
}); });
+110 -73
View File
@@ -1,28 +1,40 @@
"use strict" "use strict";
import {getColorForDistanceAndToken, getMovedDistanceFromToken, getRangesFromSpeedProvider, initApi, registerModule, registerSystem} from "./api.js"; import {
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 {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";
CONFIG.debug.dragRuler = false; CONFIG.debug.dragRuler = false;
export let debugGraphics = undefined; export let debugGraphics = undefined;
Hooks.once("init", () => { Hooks.once("init", () => {
registerSettings() registerSettings();
registerKeybindings() registerKeybindings();
initApi() initApi();
hookDragHandlers(Token); hookDragHandlers(Token);
hookDragHandlers(MeasuredTemplate); hookDragHandlers(MeasuredTemplate);
libWrapper.register("drag-ruler", "TokenLayer.prototype.undoHistory", tokenLayerUndoHistory, "WRAPPER"); libWrapper.register(
"drag-ruler",
"TokenLayer.prototype.undoHistory",
tokenLayerUndoHistory,
"WRAPPER",
);
extendRuler(); extendRuler();
@@ -33,16 +45,15 @@ Hooks.once("init", () => {
registerSystem, registerSystem,
recalculate, recalculate,
resetMovementHistory, resetMovementHistory,
} };
}) });
Hooks.once("ready", () => { Hooks.once("ready", () => {
performMigrations() performMigrations();
checkDependencies(); checkDependencies();
Hooks.callAll("dragRuler.ready", SpeedProvider) Hooks.callAll("dragRuler.ready", SpeedProvider);
if (CONFIG.debug.dragRuler) if (CONFIG.debug.dragRuler) debugGraphics = canvas.controls.addChild(new PIXI.Container());
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 => {
@@ -50,37 +61,61 @@ 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) if (!eventHandled) oldFn(...args);
oldFn(...args);
}; };
} }
function hookDragHandlers(entityType) { function hookDragHandlers(entityType) {
const entityName = entityType.name const entityName = entityType.name;
libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftStart`, onEntityLeftDragStart, "WRAPPER"); libWrapper.register(
"drag-ruler",
`${entityName}.prototype._onDragLeftStart`,
onEntityLeftDragStart,
"WRAPPER",
);
if (entityType === Token) if (entityType === Token)
libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftMove`, onEntityLeftDragMoveSnap, "WRAPPER"); libWrapper.register(
"drag-ruler",
`${entityName}.prototype._onDragLeftMove`,
onEntityLeftDragMoveSnap,
"WRAPPER",
);
else else
libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftMove`, onEntityLeftDragMove, "WRAPPER"); libWrapper.register(
libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftDrop`, forwardIfUnahndled(onEntityDragLeftDrop), "MIXED"); "drag-ruler",
libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftCancel`, forwardIfUnahndled(onEntityDragLeftCancel), "MIXED"); `${entityName}.prototype._onDragLeftMove`,
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) {
@@ -97,16 +132,22 @@ async function tokenLayerUndoHistory(wrapped) {
function onEntityLeftDragStart(wrapped, event) { function onEntityLeftDragStart(wrapped, event) {
wrapped(event); wrapped(event);
console.warn("start", Date.now());
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 (isToken && canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(this)) if (
isToken &&
canvas.grid.isHex &&
game.modules.get("hex-size-support")?.active &&
CONFIG.hexSizeSupport.getAltSnappingFlag(this)
)
entityCenter = getHexSizeSupportTokenGridCenter(this); entityCenter = getHexSizeSupportTokenGridCenter(this);
else else entityCenter = this.center;
entityCenter = this.center; ruler.rulerOffset = {
ruler.rulerOffset = {x: entityCenter.x - event.data.origin.x, y: entityCenter.y - event.data.origin.y}; x: entityCenter.x - event.data.origin.x,
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);
@@ -121,49 +162,42 @@ 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) if (ruler.isDragRuler) onMouseMove.call(ruler, event);
onMouseMove.call(ruler, event)
} }
function onEntityDragLeftDrop(event) { function onEntityDragLeftDrop(event) {
const ruler = canvas.controls.ruler const ruler = canvas.controls.ruler;
console.warn("stop", ruler._state, Date.now());
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) if (selectedTokens.length === 0) selectedTokens.push(ruler.draggedEntity);
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) if (ruler._state === Ruler.STATES.STARTING) onMouseMove.call(ruler, event);
onMouseMove.call(ruler, event); ruler._state = Ruler.STATES.MOVING;
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) if (!ruler.draggedEntity || ruler._state === Ruler.STATES.MOVING) return false;
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) if (rightClickAction !== RightClickAction.CREATE_WAYPOINT) return false;
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();
@@ -182,14 +216,10 @@ 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")) if (!game.settings.get(settingsKey, "useGridlessRaster")) return;
return; if (!ruler.isDragRuler) return;
if (!ruler.isDragRuler) if (disableSnap) return;
return; if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS) 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;
@@ -199,7 +229,11 @@ 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.dragRulerGetRaysFromWaypoints(ruler.waypoints, destination).map(ray => {return {ray}}); const segments = ruler.constructor
.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);
@@ -207,7 +241,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;
@@ -218,12 +252,15 @@ 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.dragRulerGetRaysFromWaypoints(ruler.waypoints, destination).map(ray => {return {ray}}); const segments = ruler.constructor
.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};
@@ -236,13 +273,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;
} }
} }
} }
+12 -12
View File
@@ -1,26 +1,24 @@
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) if (game.user.isGM) await performWorldMigraionts();
await performWorldMigraionts() await performClientMigrations();
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) if (dataVersion === currentDataVersion) return;
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);
@@ -31,13 +29,15 @@ 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.get("client").getItem(`${settingsKey}.swapSpacebarRightClick`); const swapSpacebarRightClick = game.settings.storage
.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);
} }
} }
+22 -26
View File
@@ -9,38 +9,35 @@ 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) if (!combatant) return undefined;
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) if (!combat) return;
return; if (!combat.started) 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)).map(([token, rays]) => calculateUpdate(combat, token, rays)).filter(Boolean); const updates = Array.from(zip(tokens, tokenRays))
.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) if (!combatant) return;
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) {
@@ -56,7 +53,10 @@ 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), {terrainRulerInitialState: waypoints[waypoints.length - 1]?.dragRulerFinalState, enableTerrainRuler: terrainRulerAvailable}); measureDistances([{ray}], token, getTokenShape(token), {
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,23 +68,18 @@ function calculateUpdate(combat, token, rays) {
export function getMovementHistory(token) { export function getMovementHistory(token) {
const combat = game.combat; const combat = game.combat;
if (!combat) if (!combat) return [];
return [];
const combatant = combat.getCombatantByToken(token.id); const combatant = combat.getCombatantByToken(token.id);
if (!combatant) if (!combatant) return [];
return []; const dragRulerFlags = combatant.flags.dragRuler;
const dragRulerFlags = combatant.data.flags.dragRuler; if (!dragRulerFlags) return [];
if (!dragRulerFlags) if (combat.round > dragRulerFlags.trackedRound) return [];
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) if (history.length === 0) return;
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;
@@ -92,14 +87,15 @@ 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, [{_id: combatant.id, dragRulerFlags: combatant.data.flags.dragRuler}]); await updateCombatantDragRulerFlags(combat, [
{_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) if (!dragRulerFlags) return;
return;
dragRulerFlags.passedWaypoints = null; dragRulerFlags.passedWaypoints = null;
dragRulerFlags.trackedRound = null; dragRulerFlags.trackedRound = null;
dragRulerFlags.rulerState = null; dragRulerFlags.rulerState = null;
-155
View File
@@ -1,155 +0,0 @@
import {getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
import {togglePathfinding} from "./keybindings.js";
import {debugGraphics} from "./main.js";
import {settingsKey} from "./settings.js";
import {getSnapPointForTokenObj, iterPairs} from "./util.js";
let cachedNodes = undefined;
let use5105 = false;
export function isPathfindingEnabled() {
if (this.user !== game.user)
return false;
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS)
return false;
if (!game.user.isGM && !game.settings.get(settingsKey, "allowPathfinding"))
return false;
return game.settings.get(settingsKey, "autoPathfinding") != togglePathfinding;
}
export function findPath(from, to, token, previousWaypoints) {
const lastNode = calculatePath(from, to, token, previousWaypoints);
if (!lastNode)
return null;
paintPathfindingDebug(lastNode, token);
const path = [];
let currentNode = lastNode;
while (currentNode) {
// TODO Check if the distance doesn't change
if (path.length >= 2 && !stepCollidesWithWall(currentNode.node, path[path.length - 2], token))
// Replace last waypoint if the current waypoint leads to a valid path
path[path.length - 1] = {x: currentNode.node.x, y: currentNode.node.y};
else
path.push({x: currentNode.node.x, y: currentNode.node.y});
currentNode = currentNode.previous;
}
return path;
}
export function wipePathfindingCache() {
cachedNodes = undefined;
}
function getNode(pos, token, initialize=true) {
pos = {layer: 0, ...pos}; // Copy pos and set pos.layer to the default value if it's unset
if (!cachedNodes)
cachedNodes = new Array(2);
if (!cachedNodes[pos.layer])
cachedNodes[pos.layer] = new Array(Math.ceil(canvas.dimensions.height / canvas.grid.h));
if (!cachedNodes[pos.layer][pos.y])
cachedNodes[pos.layer][pos.y] = new Array(Math.ceil(canvas.dimensions.width / canvas.grid.w));
if (!cachedNodes[pos.layer][pos.y][pos.x]) {
cachedNodes[pos.layer][pos.y][pos.x] = pos;
}
const node = cachedNodes[pos.layer][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)
continue;
// TODO Work with pixels instead of grid locations
if (!stepCollidesWithWall(pos, neighborPos, token)) {
const isDiagonal = node.x !== neighborPos.x && node.y !== neighborPos.y && canvas.grid.type === CONST.GRID_TYPES.SQUARE;
let targetLayer = pos.layer;
if (use5105 && isDiagonal)
targetLayer = 1 - targetLayer;
const neighbor = getNode({...neighborPos, layer: targetLayer}, token, false);
// TODO We currently assume a cost of one or two for all transitions. Change this for difficult terrain support
let edgeCost = 1;
if (isDiagonal) {
// We charge 0.0001 more for edges to avoid unnecessary diagonal steps
edgeCost = pos.layer === 1 && targetLayer === 0 ? 2 : 1.0001;
}
node.edges.push({target: neighbor, cost: edgeCost});
}
}
}
return node;
}
function calculatePath(from, to, token, previousWaypoints) {
if (game.system.id === "pf2e")
use5105 = true;
if (canvas.grid.diagonalRule === "5105")
use5105 = true;
let startLayer = 0;
if (use5105 && canvas.grid.type === CONST.GRID_TYPES.SQUARE) {
previousWaypoints = previousWaypoints.map(w => getGridPositionFromPixelsObj(w));
startLayer = calcNoDiagonals(previousWaypoints) % 2;
}
const nextNodes = [{node: getNode({...to, layer: startLayer}, token), cost: 0, estimated: estimateCost(to, from), previous: null}];
const previousNodes = new Set();
while (nextNodes.length > 0) {
// Sort by estimated cost, high to low
// TODO Re-sorting every iteration is expensive. Think of something better
nextNodes.sort((a, b) => b.estimated - a.estimated);
// Get node with cheapest estimate
const currentNode = nextNodes.pop();
if (currentNode.node.x === from.x && currentNode.node.y === from.y)
return currentNode;
previousNodes.add(currentNode.node);
for (const edge of currentNode.node.edges) {
const neighborNode = getNode(edge.target, token);
if (previousNodes.has(neighborNode))
continue;
const neighbor = {node: neighborNode, cost: currentNode.cost + edge.cost, estimated: currentNode.cost + edge.cost + estimateCost(neighborNode, from), previous: currentNode};
const neighborIndex = nextNodes.findIndex(node => node.node === neighbor.node);
if (neighborIndex >= 0) {
// If the neighbor is cheaper to reach via the current route than through previously discovered routes, replace it
if (nextNodes[neighborIndex].cost > neighbor.cost) {
nextNodes[neighborIndex] = neighbor;
}
}
else {
nextNodes.push(neighbor);
}
}
}
}
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;
}
function estimateCost(pos, target) {
return Math.max(Math.abs(pos.x - target.x), Math.abs(pos.y - target.y));
}
function stepCollidesWithWall(from, to, token) {
const stepStart = getSnapPointForTokenObj(getPixelsFromGridPositionObj(from), token);
const stepEnd = getSnapPointForTokenObj(getPixelsFromGridPositionObj(to), token);
return canvas.walls.checkCollision(new Ray(stepStart, stepEnd));
}
function paintPathfindingDebug(lastNode, token) {
if (!CONFIG.debug.dragRuler)
return;
debugGraphics.removeChildren();
let currentNode = lastNode;
while (currentNode) {
let text = new PIXI.Text(currentNode.cost.toFixed(0));
let pixels = getSnapPointForTokenObj(getPixelsFromGridPositionObj(currentNode.node), token);
text.anchor.set(0.5, 1.0);
text.x = pixels.x;
text.y = pixels.y;
debugGraphics.addChild(text);
currentNode = currentNode.previous;
}
}
+356 -55
View File
@@ -1,15 +1,30 @@
import {currentSpeedProvider, getColorForDistanceAndToken, getRangesFromSpeedProvider} from "./api.js"; import {
import {getHexSizeSupportTokenGridCenter} from "./compatibility.js"; currentSpeedProvider,
import {cancelScheduledMeasurement, measure} from "./foundry_imports.js" 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 {getMovementHistory} from "./movement_tracking.js";
import {wipePathfindingCache} from "./pathfinding.js";
import {settingsKey} from "./settings.js"; import {settingsKey} from "./settings.js";
import {getSnapPointForEntity} from "./util.js"; import {
applyTokenSizeOffset,
getSnapPointForEntity,
getSnapPointForTokenObj,
getTokenShape,
isPathfindingEnabled,
} from "./util.js";
export function extendRuler() { export function extendRuler() {
class DragRulerRuler extends Ruler { class DragRulerRuler extends CONFIG.Canvas.rulerClass {
// Functions below are overridden versions of functions in Ruler // Functions below are overridden versions of functions in Ruler
constructor(user, {color=null}={}) { constructor(user, {color = null} = {}) {
super(user, {color}); super(user, {color});
this.previousWaypoints = []; this.previousWaypoints = [];
this.previousLabels = this.addChild(new PIXI.Container()); this.previousLabels = this.addChild(new PIXI.Container());
@@ -25,8 +40,7 @@ export function extendRuler() {
async moveToken(event) { async moveToken(event) {
// Disable moveToken if Drag Ruler is active // Disable moveToken if Drag Ruler is active
if (!this.isDragRuler) if (!this.isDragRuler) return await super.moveToken(event);
return await super.moveToken(event);
} }
toJSON() { toJSON() {
@@ -46,49 +60,305 @@ export function extendRuler() {
update(data) { update(data) {
// Don't show a GMs drag ruler to non GM players // 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")) if (
data.draggedEntity &&
this.user.isGM &&
!game.user.isGM &&
!game.settings.get(settingsKey, "showGMRulerToPlayers")
)
return; return;
if (data.draggedEntity) { if (data.draggedEntity) {
if (data.draggedEntityIsToken) if (data.draggedEntityIsToken) this.draggedEntity = canvas.tokens.get(data.draggedEntity);
this.draggedEntity = canvas.tokens.get(data.draggedEntity); else this.draggedEntity = canvas.templates.get(data.draggedEntity);
else } else {
this.draggedEntity = canvas.templates.get(data.draggedEntity); this.draggedEntity = undefined;
} }
super.update(data); super.update(data);
} }
measure(destination, options={}) { measure(destination, options = {}) {
if (this.isDragRuler) { 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); 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() { _endMeasurement() {
super._endMeasurement(); super._endMeasurement();
this.draggedEntity = null; 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 // The functions below aren't present in the orignal Ruler class and are added by Drag Ruler
dragRulerAddWaypoint(point, options={}) { dragRulerAddWaypoint(point, options = {}) {
options.snap = options.snap ?? true; options.snap = options.snap ?? true;
if (options.snap) { if (options.snap) {
point = getSnapPointForEntity(point.x, point.y, this.draggedEntity); point = getSnapPointForEntity(point.x, point.y, this.draggedEntity);
} }
this.waypoints.push(new PIXI.Point(point.x, point.y)); this.waypoints.push(new PIXI.Point(point.x, point.y));
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle)); this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
this.waypoints.filter(waypoint => waypoint.isPathfinding).forEach(waypoint => waypoint.isPathfinding = false); this.waypoints
.filter(waypoint => waypoint.isPathfinding)
.forEach(waypoint => (waypoint.isPathfinding = false));
} }
dragRulerAddWaypointHistory(waypoints) { dragRulerAddWaypointHistory(waypoints) {
waypoints.forEach(waypoint => waypoint.isPrevious = true); waypoints.forEach(waypoint => (waypoint.isPrevious = true));
this.waypoints = this.waypoints.concat(waypoints); this.waypoints = this.waypoints.concat(waypoints);
for (const waypoint of waypoints) { for (const waypoint of waypoints) {
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle)); this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
@@ -100,30 +370,51 @@ export function extendRuler() {
this.labels.removeChildren().forEach(c => c.destroy()); this.labels.removeChildren().forEach(c => c.destroy());
} }
dragRulerDeleteWaypoint(event={preventDefault: () => {return}}, options={}) { dragRulerDeleteWaypoint(
event = {
preventDefault: () => {
return;
},
},
options = {},
) {
this.dragRulerRemovePathfindingWaypoints(); this.dragRulerRemovePathfindingWaypoints();
options.snap = options.snap ?? true; options.snap = options.snap ?? true;
if (this.waypoints.filter(w => !w.isPrevious).length > 1) { if (this.waypoints.filter(w => !w.isPrevious).length > 1) {
event.preventDefault(); event.preventDefault();
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens); const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(
canvas.tokens,
);
const rulerOffset = this.rulerOffset; const rulerOffset = this.rulerOffset;
// Options are not passed to _removeWaypoint in vanilla Foundry. // 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) // 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); this._removeWaypoint(
{x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y},
options,
);
game.user.broadcastActivity({ruler: this}); game.user.broadcastActivity({ruler: this});
} } else {
else {
this.dragRulerAbortDrag(event); this.dragRulerAbortDrag(event);
} }
} }
dragRulerRemovePathfindingWaypoints() { dragRulerRemovePathfindingWaypoints() {
this.waypoints.filter(waypoint => waypoint.isPathfinding).forEach(_ => this.labels.removeChild(this.labels.children[this.labels.children.length - 1]).destroy()); 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); this.waypoints = this.waypoints.filter(waypoint => !waypoint.isPathfinding);
} }
dragRulerAbortDrag(event={preventDefault: () => {return}}) { dragRulerAbortDrag(
event = {
preventDefault: () => {
return;
},
},
) {
const token = this.draggedEntity; const token = this.draggedEntity;
this._endMeasurement(); this._endMeasurement();
@@ -137,10 +428,8 @@ export function extendRuler() {
} }
async dragRulerRecalculate(tokenIds) { async dragRulerRecalculate(tokenIds) {
if (this._state !== Ruler.STATES.MEASURING) if (this._state !== Ruler.STATES.MEASURING) return;
return; if (tokenIds && !tokenIds.includes(this.draggedEntity.id)) return;
if (tokenIds && !tokenIds.includes(this.draggedEntity.id))
return;
const waypoints = this.waypoints.filter(waypoint => !waypoint.isPrevious); const waypoints = this.waypoints.filter(waypoint => !waypoint.isPrevious);
this.dragRulerClearWaypoints(); this.dragRulerClearWaypoints();
if (game.settings.get(settingsKey, "enableMovementHistory")) if (game.settings.get(settingsKey, "enableMovementHistory"))
@@ -153,8 +442,7 @@ export function extendRuler() {
} }
static dragRulerGetRaysFromWaypoints(waypoints, destination) { static dragRulerGetRaysFromWaypoints(waypoints, destination) {
if ( destination ) if (destination) waypoints = waypoints.concat([destination]);
waypoints = waypoints.concat([destination]);
return waypoints.slice(1).map((wp, i) => { return waypoints.slice(1).map((wp, i) => {
const ray = new Ray(waypoints[i], wp); const ray = new Ray(waypoints[i], wp);
ray.isPrevious = Boolean(waypoints[i].isPrevious); ray.isPrevious = Boolean(waypoints[i].isPrevious);
@@ -163,52 +451,65 @@ export function extendRuler() {
} }
dragRulerGetColorForDistance(distance) { dragRulerGetColorForDistance(distance) {
if (!this.isDragRuler) if (!this.isDragRuler) return this.color;
return this.color;
if (!this.draggedEntity.actor) { if (!this.draggedEntity.actor) {
return this.color; return this.color;
} }
// Don't apply colors if the current user doesn't have at least observer permissions // Don't apply colors if the current user doesn't have at least observer permissions
if (this.draggedEntity.actor.permission < 2) { if (this.draggedEntity.actor.permission < 2) {
// If this is a pc and alwaysShowSpeedForPCs is enabled we show the color anyway // 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"))) if (
!(
this.draggedEntity.actor.data.type === "character" &&
game.settings.get(settingsKey, "alwaysShowSpeedForPCs")
)
)
return this.color; return this.color;
} }
distance = Math.round(distance * 100) / 100; distance = Math.round(distance * 100) / 100;
if (!this.dragRulerRanges) if (!this.dragRulerRanges)
this.dragRulerRanges = getRangesFromSpeedProvider(this.draggedEntity); this.dragRulerRanges = getRangesFromSpeedProvider(this.draggedEntity);
return getColorForDistanceAndToken(distance, this.draggedEntity, this.dragRulerRanges) ?? this.color; return (
getColorForDistanceAndToken(distance, this.draggedEntity, this.dragRulerRanges) ??
this.color
);
} }
dragRulerStart(options, measureImmediately=true) { dragRulerStart(options, measureImmediately = true) {
const entity = this.draggedEntity; const entity = this.draggedEntity;
const isToken = entity instanceof Token; const isToken = entity instanceof Token;
if (isToken && !currentSpeedProvider.usesRuler(entity)) if (isToken && !currentSpeedProvider.usesRuler(entity)) return;
return;
const ruler = canvas.controls.ruler; const ruler = canvas.controls.ruler;
ruler.clear(); ruler.clear();
wipePathfindingCache();
ruler._state = Ruler.STATES.STARTING; ruler._state = Ruler.STATES.STARTING;
let entityCenter; let entityCenter;
if (isToken && canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(entity)) if (
isToken &&
canvas.grid.isHex &&
game.modules.get("hex-size-support")?.active &&
CONFIG.hexSizeSupport.getAltSnappingFlag(entity)
)
entityCenter = getHexSizeSupportTokenGridCenter(entity); entityCenter = getHexSizeSupportTokenGridCenter(entity);
else else entityCenter = entity.center;
entityCenter = entity.center;
if (isToken && game.settings.get(settingsKey, "enableMovementHistory")) if (isToken && game.settings.get(settingsKey, "enableMovementHistory"))
ruler.dragRulerAddWaypointHistory(getMovementHistory(entity)); ruler.dragRulerAddWaypointHistory(getMovementHistory(entity));
ruler.dragRulerAddWaypoint(entityCenter, {snap: false}); ruler.dragRulerAddWaypoint(entityCenter, {snap: false});
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens); const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(
const destination = {x: mousePosition.x + ruler.rulerOffset.x, y: mousePosition.y + ruler.rulerOffset.y}; canvas.tokens,
if (measureImmediately) );
ruler.measure(destination, options); const destination = {
x: mousePosition.x + ruler.rulerOffset.x,
y: mousePosition.y + ruler.rulerOffset.y,
};
if (measureImmediately) ruler.measure(destination, options);
} }
dragRulerSendState() { dragRulerSendState() {
game.user.broadcastActivity({ game.user.broadcastActivity({
ruler: this.toJSON() ruler: this.toJSON(),
}); });
} }
} }
Ruler = DragRulerRuler; CONFIG.Canvas.rulerClass = DragRulerRuler;
} }
+113 -97
View File
@@ -1,28 +1,37 @@
import {availableSpeedProviders, currentSpeedProvider, getDefaultSpeedProvider, updateSpeedProvider} from "./api.js"; import {
import {SpeedProvider} from "./speed_provider.js" availableSpeedProviders,
import { early_isGM } from "./util.js"; currentSpeedProvider,
getDefaultSpeedProvider,
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() {
window.setTimeout(() => location.reload(), 500);
}
export function registerSettings() { export function registerSettings() {
game.settings.register(settingsKey, "dataVersion", { game.settings.register(settingsKey, "dataVersion", {
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", {
@@ -63,7 +72,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",
@@ -72,7 +81,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",
@@ -83,6 +92,7 @@ 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",
@@ -90,17 +100,18 @@ export function registerSettings() {
config: true, config: true,
type: Boolean, type: Boolean,
default: false, default: false,
onChange: () => location.reload(), 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",
scpoe: "client", scope: "client",
config: early_isGM() || game.settings.get(settingsKey, "allowPathfinding"), config: early_isGM() || game.settings.get(settingsKey, "allowPathfinding"),
type: Boolean, type: Boolean,
defualt: false, default: false,
}); });
}
game.settings.register(settingsKey, "lastTerrainRulerHintTime", { game.settings.register(settingsKey, "lastTerrainRulerHintTime", {
config: false, config: false,
@@ -121,7 +132,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",
@@ -130,7 +141,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 {
@@ -140,43 +151,44 @@ 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) if (provider.hasSettings) provider.settings = enumerateProviderSettings(speedProvider);
provider.settings = enumerateProviderSettings(speedProvider) let dotPosition = provider.id.indexOf(".");
let dotPosition = provider.id.indexOf(".") if (dotPosition === -1) dotPosition = provider.id.length;
if (dotPosition === -1) const type = provider.id.substring(0, dotPosition);
dotPosition = provider.id.length const id = provider.id.substring(dotPosition + 1);
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("drag-ruler.settings.speedProviderSettings.speedProvider.choices.native") provider.selectTitle = game.i18n.localize(
} "drag-ruler.settings.speedProviderSettings.speedProvider.choices.native",
else { );
let name } else {
let name;
if (type === "module") { if (type === "module") {
name = game.modules.get(id).data.title name = game.modules.get(id).data.title;
} else {
name = game.system.data.title;
} }
else { provider.selectTitle = game.i18n.format(
name = game.system.data.title `drag-ruler.settings.speedProviderSettings.speedProvider.choices.${type}`,
{name},
);
} }
provider.selectTitle = game.i18n.format(`drag-ruler.settings.speedProviderSettings.speedProvider.choices.${type}`, {name}) provider.isSelected = provider.id === selectedProvider;
} return provider;
provider.isSelected = provider.id === selectedProvider });
return provider data.selectedProviderName = data.providers.find(provider => provider.isSelected).selectTitle;
})
data.selectedProviderName = data.providers.find(provider => provider.isSelected).selectTitle
data.providerSelection = { data.providerSelection = {
id: "speedProvider", id: "speedProvider",
@@ -184,122 +196,126 @@ 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 ? formData.speedProvider : game.settings.get(settingsKey, "speedProvider") const selectedSpeedProvider = game.user.isGM
? 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") if (splitKey[0] !== "native") splitKey.shift();
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)) if (key !== "speedProvider" && !key.startsWith(selectedSpeedProvider)) continue;
continue
// Get the key for the current setting // Get the key for the current setting
let setting let setting;
if (key === "speedProvider") if (key === "speedProvider") setting = "speedProvider";
setting = "speedProvider" else setting = `speedProviders.${key}`;
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) if (value !== oldValue) game.settings.set(settingsKey, setting, value);
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.querySelectorAll(".drag-ruler-provider-settings").forEach(element => element.style.display = "none") document
.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 = {id: "unreachable", name: "drag-ruler.settings.speedProviderSettings.color.unreachable.name"} const unreachableColor = {
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(game.settings.get(settingsKey, `speedProviders.${provider.id}.color.${color.id}`)), value: toDomHex(
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) if (setting.scope === "world" && !game.user.isGM) continue;
continue const s = duplicate(setting);
const s = duplicate(setting) s.id = `${provider.id}.setting.${s.id}`;
s.id = `${provider.id}.setting.${s.id}` s.name = game.i18n.localize(s.name);
s.name = game.i18n.localize(s.name) s.hint = game.i18n.localize(s.hint);
s.hint = game.i18n.localize(s.hint) s.value = provider.getSetting(setting.id);
s.value = provider.getSetting(setting.id) s.type = setting.type instanceof Function ? setting.type.name : "String";
s.type = setting.type instanceof Function ? setting.type.name : "String" s.isCheckbox = setting.type === Boolean;
s.isCheckbox = setting.type === Boolean s.isSelect = s.choices !== undefined;
s.isSelect = s.choices !== undefined s.isRange = setting.type === Number && s.range;
s.isRange = (setting.type === Number) && s.range s.isColor = false;
s.isColor = false settings.push(s);
settings.push(s) } catch (e) {
} console.warn(
catch (e) { `Drag Ruler | The following error occured while rendering setting "${setting.id}" of module/system "${this.id}. It won't be displayed.`,
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);
} }
+15 -6
View File
@@ -11,8 +11,13 @@ 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.executeAsGM(_socketUpdateCombatantDragRulerFlags, combatId, updates) return socket
.then(() => currentSpeedProvider.onMovementHistoryUpdate(updates.map(update => canvas.tokens.get(combat.combatants.get(update._id).token.id)))); .executeAsGM(_socketUpdateCombatantDragRulerFlags, combatId, updates)
.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) {
@@ -21,12 +26,15 @@ 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) if (!actor) return false;
return false;
return actor.testUserPermission(user, "OWNER"); return actor.testUserPermission(user, "OWNER");
}); });
if (updates.length !== requestedUpdates) { if (updates.length !== requestedUpdates) {
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`); 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`,
);
} }
updates = updates.map(update => { updates = updates.map(update => {
return {_id: update._id, flags: {dragRuler: update.dragRulerFlags}}; return {_id: update._id, flags: {dragRuler: update.dragRulerFlags}};
@@ -39,5 +47,6 @@ export function recalculate(tokens) {
} }
function _socketRecalculate(tokenIds) { function _socketRecalculate(tokenIds) {
return canvas.controls.ruler.dragRulerRecalculate(tokenIds); const ruler = canvas.controls.ruler;
if (ruler.isDragRuler) ruler.dragRulerRecalculate(tokenIds);
} }
+31 -49
View File
@@ -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,28 +53,7 @@ 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))
} }
/** /**
@@ -86,7 +65,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;
} }
/** /**
@@ -104,13 +83,14 @@ 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(`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.`) 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.`,
);
} }
} }
@@ -120,32 +100,34 @@ 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) if (!speedAttribute) return [];
return []
const tokenSpeed = parseFloat(getProperty(token, speedAttribute)); const tokenSpeed = parseFloat(getProperty(token, speedAttribute));
if (tokenSpeed === undefined) { if (tokenSpeed === undefined) {
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).`) console.warn(
return [] `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 [];
} }
const dashMultiplier = this.getSetting("dashMultiplier") const dashMultiplier = this.getSetting("dashMultiplier");
if (!dashMultiplier) if (!dashMultiplier) return [{range: tokenSpeed, color: "walk"}];
return [{range: tokenSpeed, color: "walk"}] return [
return [{range: tokenSpeed, color: "walk"}, {range: tokenSpeed * dashMultiplier, color: "dash"}] {range: tokenSpeed, color: "walk"},
{range: tokenSpeed * dashMultiplier, color: "dash"},
];
} }
get settings() { get settings() {
@@ -167,7 +149,7 @@ export class GenericSpeedProvider extends SpeedProvider {
config: true, config: true,
type: Number, type: Number,
default: getDefaultDashMultiplier(), default: getDefaultDashMultiplier(),
} },
] ];
} }
} }
+20 -13
View File
@@ -1,33 +1,38 @@
export function getDefaultSpeedAttribute() { export function getDefaultSpeedAttribute() {
switch (game.system.id) { switch (game.system.id) {
case "CoC7": case "CoC7":
return "actor.data.data.attribs.mov.value"; return "actor.system.attribs.mov.value";
case "dcc": case "dcc":
return "actor.data.data.attributes.speed.value"; return "actor.system.attributes.speed.value";
case "dnd4e":
return "actor.system.movement.walk.value";
case "dnd5e": case "dnd5e":
return "actor.data.data.attributes.movement.walk" return "actor.system.attributes.movement.walk";
case "lancer": case "lancer":
return "actor.data.data.derived.speed" return "actor.system.derived.speed";
case "pf1": case "pf1":
case "D35E": case "D35E":
return "actor.data.data.attributes.speed.land.total" return "actor.system.attributes.speed.land.total";
case "sfrpg": case "sfrpg":
return "actor.data.data.attributes.speed.value"; return "actor.system.attributes.speed.value";
case "shadowrun5e": case "shadowrun5e":
return "actor.data.data.movement.walk.value"; return "actor.system.movement.walk.value";
case "swade": case "swade":
return "actor.data.data.stats.speed.adjusted"; return "actor.system.stats.speed.adjusted";
case "ds4": case "ds4":
return "actor.data.data.combatValues.movement.total"; return "actor.system.combatValues.movement.total";
case "splittermond":
return "actor.derivedValues.speed.value";
} }
return "" return "";
} }
export function getDefaultDashMultiplier() { export function getDefaultDashMultiplier() {
switch (game.system.id) { switch (game.system.id) {
case "swade": case "swade":
return 0 return 0;
case "dcc": case "dcc":
case "dnd4e":
case "dnd5e": case "dnd5e":
case "lancer": case "lancer":
case "pf1": case "pf1":
@@ -35,9 +40,11 @@ export function getDefaultDashMultiplier() {
case "sfrpg": case "sfrpg":
case "shadowrun5e": case "shadowrun5e":
case "ds4": case "ds4":
return 2 return 2;
case "CoC7": case "CoC7":
return 5; return 5;
case "splittermond":
return 3;
} }
return 0 return 0;
} }
+124 -101
View File
@@ -1,9 +1,11 @@
import {getPixelsFromGridPosition} from "./foundry_fixes.js" import {getPixelsFromGridPosition} from "./foundry_fixes.js";
import { disableSnap } from "./keybindings.js"; import {findVertexSnapPoint} from "./hex_support.js";
import {disableSnap, moveWithoutAnimation, togglePathfinding} 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]];
} }
} }
@@ -16,7 +18,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]];
} }
} }
@@ -25,62 +27,80 @@ export function sum(arr) {
return arr.reduce((a, b) => a + b, 0); return arr.reduce((a, b) => a + b, 0);
} }
export function buildSnapPointTokenData(token) {
const tokenData = {
width: token.document.width,
height: token.document.height,
};
if (isModuleActive("hex-size-support")) {
tokenData.hexSizeSupport = {};
tokenData.hexSizeSupport.altSnappingFlag = CONFIG.hexSizeSupport.getAltSnappingFlag(token);
tokenData.hexSizeSupport.altOrientationFlag =
CONFIG.hexSizeSupport.getAltOrientationFlag(token);
tokenData.hexSizeSupport.borderSize = token.document.getFlag("hex-size-support", "borderSize");
}
return tokenData;
}
export function getSnapPointForToken(x, y, token) { export function getSnapPointForToken(x, y, token) {
return getSnapPointForTokenData(x, y, buildSnapPointTokenData(token));
}
export function getSnapPointForTokenDataObj(pos, tokenData) {
return getSnapPointForTokenData(pos.x, pos.y, tokenData);
}
// A copy of this function lives in the librouting module
function getSnapPointForTokenData(x, y, tokenData) {
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) { if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
return new PIXI.Point(x, y); return new PIXI.Point(x, y);
} }
if (canvas.grid.isHex) { if (canvas.grid.isHex) {
if (game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) { if (tokenData.hexSizeSupport?.altSnappingFlag) {
if (token.document.getFlag("hex-size-support", "borderSize") % 2 === 0) { if (tokenData.hexSizeSupport.borderSize % 2 === 0) {
const snapPoint = CONFIG.hexSizeSupport.findVertexSnapPoint(x, y, token, canvas.grid.grid) const snapPoint = findVertexSnapPoint(x, y, tokenData.hexSizeSupport.altOrientationFlag);
return new PIXI.Point(snapPoint.x, snapPoint.y) return new PIXI.Point(snapPoint.x, snapPoint.y);
} else {
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 (token.data.width % 2 === 0) if (tokenData.width % 2 === 0) cellX = x - canvas.grid.h / 2;
cellX = x - canvas.grid.h / 2; else cellX = x;
else if (tokenData.height % 2 === 0) cellY = y - canvas.grid.h / 2;
cellX = x; else cellY = y;
if (token.data.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
if (token.data.width <= 0.5) { if (tokenData.width <= 0.5) {
const offsetX = x - topLeftX; const offsetX = x - topLeftX;
const subGridWidth = Math.floor(canvas.grid.w / 2); const subGridWidth = Math.floor(canvas.grid.w / 2);
const subGridPosX = Math.floor(offsetX / subGridWidth); const subGridPosX = Math.floor(offsetX / subGridWidth);
snapX = topLeftX + (subGridPosX + 0.5) * subGridWidth; snapX = topLeftX + (subGridPosX + 0.5) * subGridWidth;
} }
// Tokens with odd multipliers (1x1, 3x3, ...) and tokens smaller than 1x1 but bigger than 0.5x0.5 snap to the center of the grid cell // Tokens with odd multipliers (1x1, 3x3, ...) and tokens smaller than 1x1 but bigger than 0.5x0.5 snap to the center of the grid cell
else if (Math.round(token.data.width) % 2 === 1 || token.data.width < 1) { else if (Math.round(tokenData.width) % 2 === 1 || tokenData.width < 1) {
snapX = centerX; snapX = centerX;
} }
// All remaining tokens (those with even or fractional multipliers on square grids) snap to the intersection points of the grid // All remaining tokens (those with even or fractional multipliers on square grids) snap to the intersection points of the grid
else { else {
snapX = centerX + canvas.grid.w / 2; snapX = centerX + canvas.grid.w / 2;
} }
if (token.data.height <= 0.5) { if (tokenData.height <= 0.5) {
const offsetY = y - topLeftY; const offsetY = y - topLeftY;
const subGridHeight = Math.floor(canvas.grid.h / 2); const subGridHeight = Math.floor(canvas.grid.h / 2);
const subGridPosY = Math.floor(offsetY / subGridHeight); const subGridPosY = Math.floor(offsetY / subGridHeight);
snapY = topLeftY + (subGridPosY + 0.5) * subGridHeight; snapY = topLeftY + (subGridPosY + 0.5) * subGridHeight;
} } else if (Math.round(tokenData.height) % 2 === 1 || tokenData.height < 1) {
else if (Math.round(token.data.height) % 2 === 1 || token.data.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);
@@ -94,37 +114,18 @@ export function getSnapPointForMeasuredTemplate(x, y) {
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);
} }
let subgridWidth, subgridHeight; return canvas.grid.grid.getSnappedPosition(x, y, canvas.templates.gridPrecision);
if (canvas.grid.type === CONST.GRID_TYPES.SQUARE) {
subgridWidth = subgridHeight = canvas.dimensions.size / 2;
}
else {
if (canvas.grid.grid.columns) {
subgridWidth = canvas.grid.w / 4;
subgridHeight = canvas.grid.h / 2;
}
else {
subgridWidth = canvas.grid.w / 2;
subgridHeight = canvas.grid.h / 4;
}
}
const snappedX = Math.round(x / subgridWidth) * subgridWidth;
const snappedY = Math.round(y / subgridHeight) * subgridHeight;
return new PIXI.Point(snappedX, snappedY);
} }
export function getSnapPointForEntity(x, y, entity) { export function getSnapPointForEntity(x, y, entity) {
const isToken = entity instanceof Token; const isToken = entity instanceof Token;
if (isToken) if (isToken) return getSnapPointForToken(x, y, entity);
return getSnapPointForToken(x, y, entity); else return getSnapPointForMeasuredTemplate(x, y);
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 ) if (!layer) return false;
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);
@@ -138,59 +139,73 @@ 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) if (canvas.grid.grid.options.even) shiftedRow = 1;
shiftedRow = 1 else shiftedRow = 0;
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};
}); });
} }
export function getTokenShape(token) { export function getTokenShape(token) {
if (token.scene.data.gridType === CONST.GRID_TYPES.GRIDLESS) { return getTokenShapeForTokenData(buildSnapPointTokenData(token), token.scene);
return [{x: 0, y: 0}] }
}
else if (token.scene.data.gridType === CONST.GRID_TYPES.SQUARE) { export function getTokenShapeForTokenData(tokenData, scene = canvas.scene) {
const topOffset = -Math.floor(token.data.height / 2) if (scene.grid.type === CONST.GRID_TYPES.GRIDLESS) {
const leftOffset = -Math.floor(token.data.width / 2) return [{x: 0, y: 0}];
const shape = [] } else if (scene.grid.type === CONST.GRID_TYPES.SQUARE) {
for (let y = 0;y < token.data.height;y++) { const topOffset = -Math.floor(tokenData.height / 2);
for (let x = 0;x < token.data.width;x++) { const leftOffset = -Math.floor(tokenData.width / 2);
shape.push({x: x + leftOffset, y: y + topOffset}) const shape = [];
for (let y = 0; y < tokenData.height; y++) {
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 && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) { if (game.modules.get("hex-size-support")?.active && tokenData.hexSizeSupport.altSnappingFlag) {
const borderSize = token.data.flags["hex-size-support"].borderSize; const borderSize = tokenData.hexSizeSupport.borderSize;
let shape = [{x: 0, y: 0}]; let shape = [{x: 0, y: 0}];
if (borderSize >= 2) if (borderSize >= 2)
shape = shape.concat([{x: 0, y: -1}, {x: -1, y: -1}]); shape = shape.concat([
{x: 0, y: -1},
{x: -1, y: -1},
]);
if (borderSize >= 3) if (borderSize >= 3)
shape = shape.concat([{x: 0, y: 1}, {x: -1, y: 1}, {x: -1, y: 0}, {x: 1, y: 0}]); shape = shape.concat([
{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([{x: -2, y: -1}, {x: 1, y: -1}, {x: -1, y: -2}, {x: 0, y: -2}, {x: 1, y: -2}]) shape = shape.concat([
{x: -2, y: -1},
{x: 1, y: -1},
{x: -1, y: -2},
{x: 0, y: -2},
{x: 1, y: -2},
]);
if (Boolean(CONFIG.hexSizeSupport.getAltOrientationFlag(token)) !== canvas.grid.grid.options.columns) if (Boolean(tokenData.hexSizeSupport.altOrientationFlag) !== canvas.grid.grid.options.columns)
shape.forEach(space => space.y *= -1); shape.forEach(space => (space.y *= -1));
if (canvas.grid.grid.options.columns) if (canvas.grid.grid.options.columns)
shape = shape.map(space => {return {x: space.y, y: space.x}}); shape = shape.map(space => {
return {x: space.y, y: space.x};
});
return shape; return shape;
} } else {
else {
return [{x: 0, y: 0}]; return [{x: 0, y: 0}];
} }
} }
@@ -198,13 +213,12 @@ export function getTokenShape(token) {
export function getTokenSize(token) { export function getTokenSize(token) {
let w, h; let w, h;
const hexSizeSupportBorderSize = token.data.flags["hex-size-support"]?.borderSize; const hexSizeSupportBorderSize = token.document.flags["hex-size-support"]?.borderSize;
if (hexSizeSupportBorderSize > 0) { if (hexSizeSupportBorderSize > 0) {
w = h = hexSizeSupportBorderSize w = h = hexSizeSupportBorderSize;
} } else {
else { w = token.document.width;
w = token.data.width h = token.document.height;
h = token.data.height
} }
return {w, h}; return {w, h};
} }
@@ -213,7 +227,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);
@@ -224,21 +238,17 @@ 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) if (!isAltOrientation) waypointOffset.x *= -1;
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) if (isAltOrientation) waypointOffset.y *= -1;
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;
} }
@@ -247,7 +257,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) {
@@ -256,8 +266,7 @@ 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;
} }
} }
@@ -267,7 +276,9 @@ export function isClose(a, b, delta) {
} }
export function getMeasurePosition() { export function getMeasurePosition() {
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens); const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(
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;
@@ -279,3 +290,15 @@ export function early_isGM() {
const gmLevel = CONST.USER_ROLES.ASSISTANT; const gmLevel = CONST.USER_ROLES.ASSISTANT;
return level >= gmLevel; return level >= gmLevel;
} }
export function isModuleActive(moduleName) {
return game.modules.get(moduleName)?.active;
}
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;
}