Compare commits

..

87 Commits

Author SHA1 Message Date
Manuel Vögele d1367aa82e Release v1.6.5 2021-05-15 20:05:50 +02:00
Manuel Vögele 9f70e5bc27 If not token is selected, use the dragged token as selected token (fixes #70) 2021-05-15 20:04:22 +02:00
Manuel Vögele 334ccbc0f1 Release v1.6.4 2021-05-10 09:13:32 +02:00
Manuel Vögele 1242035744 Catch all errors thrown by a Speed Provider's getCostForStep function 2021-05-10 09:11:29 +02:00
Manuel Vögele 49542a68e5 Simplify foundry fixes 2021-05-06 09:00:50 +02:00
Chris Sharp 27a6235cc1 Mention WWII:OWB Operation WhiteBox system support in the readme 2021-05-05 23:40:40 +02:00
Manuel Vögele c26b39c984 Release v1.6.3 2021-05-05 16:01:52 +02:00
Manuel Vögele e8ab77a62e Update active rulers when the movement history changes 2021-05-05 16:01:13 +02:00
Manuel Vögele 7bed5abd0a Add default settings for shadowrun 5e 2021-05-03 19:39:22 +02:00
Manuel Vögele 4a96348659 Add "Tagmar RPG" and "Shadow of the Demon Lord" to the List of game systems with Drag Ruler integration 2021-05-03 16:00:34 +02:00
Manuel Vögele bed16de575 Release v1.6.2 2021-05-03 10:53:53 +02:00
Manuel Vögele 891bb1b4d8 undefined cannot be transmitted via json, so we use null to reset the movement history instead 2021-05-03 10:52:57 +02:00
Manuel Vögele 43f26088b5 Release v1.6.1 2021-04-30 10:52:00 +02:00
Manuel Vögele 39a0787c79 Add documentation about what needs to be awaited in SpeedProvider.onMovementHistoryUpdate 2021-04-30 10:26:37 +02:00
Manuel Vögele 3be898e49c First move the token, then store it's path. This ensures that getMovedDistance always returns a valid result. 2021-04-30 09:53:53 +02:00
Manuel Vögele 817662bf30 Send a proper token to onMovementHistoryUpdate 2021-04-30 09:51:05 +02:00
Manuel Vögele 56527ccf75 Add a hook that's called when the movement history is updated to the API 2021-04-30 00:10:45 +02:00
Manuel Vögele 6bf8083f7a Add a function for resetting the movement history to the public api 2021-04-30 00:00:15 +02:00
Manuel Vögele b5ea1f2284 Release v1.6.0 2021-04-29 14:04:14 +02:00
Manuel Vögele 6d9870bedb Add reference to the github issue in changelog 2021-04-29 13:24:33 +02:00
Manuel Vögele afbe5f9473 Don't animate tokens when they are dropped with the alt key being pressed (resolves #3) 2021-04-29 13:18:35 +02:00
Manuel Vögele bae0e43b7b Document 70b166d8 in changelog 2021-04-29 12:46:25 +02:00
Manuel Vögele 1acc012d65 Cache the output of SpeedProvider.getRanges during a drag to increase speed provider performance 2021-04-29 12:40:17 +02:00
Manuel Vögele 4671e6de51 Only allow users to update the movement history for tokens they own 2021-04-29 00:20:24 +02:00
Manuel Vögele f0ef109658 Track the movement of all tokens that are moved simultaneously in one batch (huge performance bump if many tokens are moved) 2021-04-29 00:03:05 +02:00
Manuel Vögele f0d1ef9d48 There might not actually be a dragged token when trying to end the measurement 2021-04-28 23:25:22 +02:00
Manuel Vögele d04ea9b0b7 Remove leftover lines from a refactor that didn't do anything 2021-04-28 12:11:58 +02:00
Manuel Vögele ba8ab9d473 After moving a token only end the measurement if no new token is being dragged 2021-04-27 21:17:28 +02:00
Manuel Vögele 2691720090 Update all moved tokens at once (provides a huge performance bump) 2021-04-27 20:28:44 +02:00
Manuel Vögele 70b166d844 Add a context menu entry that the gm can use to reset the movement history 2021-04-26 11:09:56 +02:00
Manuel Vögele 908600bfa3 Release v1.5.4 2021-04-21 10:08:50 +02:00
Manuel Vögele 6472c1d5bd Don't include the movement history in collission checks (fixes #61) 2021-04-21 09:05:50 +02:00
Manuel Vögele 9151b45874 Release v1.5.3 2021-04-20 17:03:13 +02:00
Manuel Vögele d732f7ca17 Add defaults for dcc system 2021-04-20 17:02:37 +02:00
Manuel Vögele 4be6730fd5 Use parseInt on the speed attribute to increase compatibility with game systems (resolves #60) 2021-04-20 17:00:28 +02:00
Manuel Vögele 1faeda6f47 Release v1.5.2 2021-04-19 11:23:26 +02:00
Manuel Vögele 667259d5d6 If a token has no actor, use the user's color for highlighting grid spaces (fixes #58) 2021-04-19 11:22:13 +02:00
Manuel Vögele 4b68b82590 Release v1.5.1 2021-04-15 17:19:32 +02:00
Manuel Vögele 447995977b Don't show the difficult terrain measurement hint when no terrain module is enabled 2021-04-15 17:17:38 +02:00
Manuel Vögele 9e5b344b0d Release v1.5.0 2021-04-14 22:51:05 +02:00
Manuel Vögele 43c5df42d4 If a terrain module is installed, suggest installing Terrain Ruler to the user 2021-04-14 22:49:18 +02:00
Manuel Vögele d0ee47d551 Fix typo that made regular rulers not work 2021-04-14 19:45:17 +02:00
Manuel Vögele 27ffb2d8cb Don't return any value in _socketUpdateCombatantDragRulerFlags to reduce the amount of network traffic 2021-04-14 17:32:40 +02:00
Manuel Vögele b734257515 Check if all required dependencies are present and show an error if this is not the case 2021-04-14 16:37:46 +02:00
Manuel Vögele ff5d46ebbe Add a setting to disable the movement history 2021-04-14 16:10:11 +02:00
Manuel Vögele 8f896d9b82 Fix a bug that caused Drag Ruler to measure euclidic distance when Terrain Ruler isn't active 2021-04-14 15:54:59 +02:00
Manuel Vögele f7683e8945 Update the movement tracking flags in the GM client, becauase the user isn't allowed to modify them 2021-04-14 15:54:59 +02:00
Manuel Vögele 9787a41fd1 If Terran Ruler is enabled, store the length of the traveled path so changes to difficult terrain aren't reflected in the tokens movement history 2021-04-12 18:45:57 +02:00
Manuel Vögele 5d93c61f0c Merge branch 'master' into develop 2021-04-11 14:31:44 +02:00
Manuel Vögele 00e8a9781e Release v1.4.6 2021-04-11 14:27:09 +02:00
Manuel Vögele d2f2307e4d Update the ruler before dropping the token to get a more precise target location if the token has been moved very quickly (fixes #51) 2021-04-11 14:24:23 +02:00
Manuel Vögele 65912df989 Add support for the Enhanced Terrain Layer module (resolves #50) 2021-04-10 15:02:35 +02:00
Manuel Vögele 9ae9dae427 Add c66ec46 to changelog 2021-04-10 15:00:48 +02:00
Manuel Vögele 8cd5d7f276 Migrate Drag Ruler to Terrain Ruler's new API 2021-04-10 14:43:45 +02:00
Manuel Vögele 35624a37aa Add api endpoint to determine the distance that a token has moved already 2021-04-10 14:25:01 +02:00
Manuel Vögele c66ec46aed Track the path taken by tokens in combat and display it when reactivating DragRuler in the same turn 2021-04-08 09:35:04 +02:00
Manuel Vögele dec422fa3a Fix typo in changelog 2021-04-01 22:57:13 +02:00
Manuel Vögele 6ad1f4c522 Merge branch 'master' into develop 2021-04-01 22:36:06 +02:00
Manuel Vögele fb9a2ee60c Release v1.4.5 2021-04-01 22:35:23 +02:00
Manuel Vögele 57ed545e30 Snap tiny tokens (0.5x0.5 or smaller) to the corners of the grid squares like it is done in vanilla foundry (fixes #49) 2021-04-01 22:25:21 +02:00
Manuel Vögele 94fe223529 Fix measurement errors when disabling token snapping while the Terrain Ruler module isn't enabled 2021-04-01 00:07:26 +02:00
Manuel Vögele 305a6116bc Refactor: Override ruler class instead of hooking each function individually 2021-03-31 17:19:12 +02:00
Manuel Vögele 942bfa04e8 Release v1.4.4 2021-03-25 01:57:41 +01:00
Manuel Vögele b4ac65e478 Fix snapping for tokens that are smaller than 1x1 (fixes #48) 2021-03-25 01:55:48 +01:00
Manuel Vögele 0de01379e9 Release v1.4.3 2021-03-24 12:35:40 +01:00
Manuel Vögele f6eb5eff9c Add german translation 2021-03-24 12:35:05 +01:00
Manuel Vögele fee0983841 Mention 3de044e in changelog 2021-03-24 12:08:10 +01:00
Brother Sharp 3de044ea45 Update japanese translation (#47) 2021-03-24 12:05:10 +01:00
Manuel Vögele e473fea121 Add defaults for swade game system to generic speed provider 2021-03-18 19:06:35 +01:00
Manuel Vögele 0aee541eb3 Release v1.4.2 2021-03-18 07:45:18 +01:00
Manuel Vögele 40dde08ddd Allow getTokenShape on gridless maps (fixes #44) 2021-03-18 07:41:30 +01:00
Manuel Vögele 8234aa566d Release v1.4.1 2021-03-17 09:26:46 +01:00
Manuel Vögele 2862643ba8 Add missing check for whether hex-size-support module is present (fixes #43) 2021-03-17 09:25:30 +01:00
Manuel Vögele e3abdeab3a Mention chinese translation in changelog 2021-03-16 09:27:51 +01:00
zeteticl 2ff4e98fe9 Add chinese localization (#39) 2021-03-16 09:02:32 +01:00
Manuel Vögele e0d0483722 Release v1.4.0 2021-03-15 20:52:52 +01:00
Manuel Vögele 4f1dec3089 Support for difficult terrain with tokens larger than 1x1 2021-03-15 20:52:52 +01:00
Manuel Vögele c4d089e8ff Always draw the ruler from the token's center for larger tokens (even on grids). Highlight all spaces occupied by larger tokens. 2021-03-15 20:17:09 +01:00
Manuel Vögele f8fe4ee971 Allow bug reporter 2021-03-15 18:05:14 +01:00
Manuel Vögele 378401b5bb Merge branch 'master' into develop 2021-03-10 11:55:22 +01:00
Manuel Vögele 0a54a47951 Release v1.3.5 2021-03-10 11:51:41 +01:00
Manuel Vögele 0b71504da3 Fix regression introduced in 6e745820 that made colors flicker during measurement 2021-03-10 11:50:55 +01:00
Manuel Vögele 5a7f5531f2 When multiple colors overlap on one square draw the one representing the furthest range with the highest priority 2021-03-09 14:51:55 +01:00
Manuel Vögele 862477c698 Add a setting that allows hiding the GM's drag ruler from non gm players (resolves #23) 2021-03-09 12:54:28 +01:00
Manuel Vögele 384de1a1b5 Add support for difficult terrain via Terrain Ruler module (resolves #1) 2021-03-09 10:08:51 +01:00
Manuel Vögele 0a7d8495c6 Release v1.3.4 2021-03-09 09:21:09 +01:00
Manuel Vögele 6e74582089 Stop patching _highlightMeasurement and import it instead to increase compatibility with other modules 2021-03-09 09:09:50 +01:00
19 changed files with 1141 additions and 181 deletions
+132 -1
View File
@@ -1,3 +1,134 @@
## 1.6.5
### Bugfixes
- Drag Ruler no longer get's stuck if the user presses ESC during drag ([#70](https://github.com/manuelVo/foundryvtt-drag-ruler/issues/70))
## 1.6.4
### Bugfixes
- Fixed a bug where a bug in a Speed Provider could lead to the ruler getting stuck, leaving the token immovable
## 1.6.3
### Bugfixes
- If the movement history for a token is being updated (for example by a history reset by the gm) while a player is currently measuring a distance for that token the history change is now being reflected in the active measurement.
### Compatibility
- Drag Ruler's Generic SpeedProvider is now aware of good default values for the Savage Worlds Adventure Edition game system
## 1.6.2
### Bugfixes
- The reset movement history button now resets the movement history for all players, not just for the GM
## 1.6.1
### API
- Added `onMovementHistoryUpdate` callback to Speed Providers, that allows them to perform game systems specific improvements to the movement history
- Added `dragRuler.resetMovementHistory` that clears the stored movement history for a token.
## 1.6.0
### Performance
- Greatly increased the performance when playing on huge maps and when moving many tokens at once.
- Huge performance improvements for speed providers. (Technical details: `getRanges` is now being called way less frequently)
### New features
- GMs now have an option to reset the movement history for individual tokens in the right click menu of the combat tracker
- When releasing a dragged token while pressing Alt the token will be moved to the target location without an animation ([#3](https://github.com/manuelVo/foundryvtt-drag-ruler/issues/3))
### Bugfixes
- When starting to drag a new token while the previous one is still moving the ruler won't dissappear anymore when the previous token arrives at it's destination.
## 1.5.4
### Bugfixes
- Fixed a bug that prevented tokens from being moved when their movement history collides with a wall. ([#61](https://github.com/manuelVo/foundryvtt-drag-ruler/issues/61))
## 1.5.3
### Compatiblilty
- Drag Ruler's Generic Speed Provider is now able to work with game systems that put non-number characters behind the tokens movement speed (like `30ft.`). One example for such a game system is Dungeon Crawl Classics. ([#60](https://github.com/manuelVo/foundryvtt-drag-ruler/issues/60))
- The Generic Speed Provider now has good default settings for the Dungeon Crawl Classics (dcc) game system.
## 1.5.2
### Bugfixes
- Drag Ruler no longer prevents tokens that don't have an actor from being moved. ([#58](https://github.com/manuelVo/foundryvtt-drag-ruler/issues/58))
- Grid highlighting now also works for tokens that don't have an actor.
## 1.5.1
### Bugfixes
- The hint that tells users how to enable difficult terrain measurement in Drag Ruler is no longer shown if no terrain layer module is installed.
## 1.5.0
### New features
- In combat Drag Ruler will now remember the path that was taken by a token during the turn. Picking the token up during the same turn will continue the previous measurement, taking steps that are already taken into account.
### Module compatibility
- Drag Ruler is now fully compatible with the `Enhanced Terrain Layer` module. ([#50](https://github.com/manuelVo/foundryvtt-drag-ruler/issues/50))
## 1.4.6
### Bugfixes
- Fixed a bug where a token would move to the incorrect location if it is was dragged and released very quickly ([#51](https://github.com/manuelVo/foundryvtt-drag-ruler/issues/51)) - thanks to Silver Raven for helping me track down this bug!
## 1.4.5
### Bugfixes
- Tiny tokens (0.5x0.5 or smaller) now snap to the coners of a square like they do in vanilla foundry ([#49](https://github.com/manuelVo/foundryvtt-drag-ruler/issues/49))
- Fixed a bug that could cause a meausred distance to be wrong when disabling token snapping using the shift key
- Fixed a bug where the highlighted path could have gaps when disabling token snapping using the shift key
## 1.4.4
### Bugfixes
- Fix snapping for tokens that are smaller than 1x1
## 1.4.3
### System compatibility
- Drag Ruler's Generic SpeedProvider is now aware of good default values for the Savage Worlds Adventure Edition game system
### Translation
- Added german translation
- Updated japanese translation (thanks to touge)
## 1.4.2
### Bugfixes
- Drag Ruler now works again on gridless maps
## 1.4.1
### Bugfixes
- Fixed a bug where Drag Ruler wouldn't work at all on hex grids when the Hex Token Size Support isn't installed
### Translation
- Added chinese translation (thanks to zeteticl)
## 1.4.0
### New features
- If the [Terrain Ruler module](https://foundryvtt.com/packages/terrain-ruler/) is installed and activated, Drag Ruler will now take difficult terrain that was placed with the [TerrainLayer module](https://foundryvtt.com/packages/TerrainLayer/) into account.
- The ruler will now always be drawn from the tokens center (even for tokens larger than 1x1) on all grid types (on Hex Grids this requires the Hex Token Size Support module).
- For tokens larger than 1x1 the highlighted path will now reflect the tokens size (on Hex Grids this requires the Hex Token Size Support module)
- The GM's Drag Ruler can now be hidden from non GM players via a setting.
- When multiple different colors apply to a single grid space because the path crosses itself the color representing ranges further away will take priortiy over colors representing closer ranges.
### API
- Speed providers can now selectively ignore difficult terrain and even implement their own cost functions, if the default Drag Ruler behavior doesn't fit the game system. This can be achieved by overriding the new, optional `SpeedProvider` function `getCostForStep`.
## 1.3.5
### Bugfixes
- Fixed a regression where spaces could suddenly change their color during measurement
## 1.3.4
### Module compatibility
- Increased compatiblility with other modules (namely Drag Ruler and Terrain Ruler are no longer incompatible)
## 1.3.3
### Bugfixes
- Speed Provider Settings are now being saved for non GM players
@@ -5,7 +136,7 @@
## 1.3.2
### Translation
- Updated japanese translation (thanks to togue)
- Updated japanese translation (thanks to touge)
## 1.3.1
+12
View File
@@ -15,6 +15,13 @@ You can add waypoints to the path by pressing spacebar while you drag the token.
![Demonstration of Waypoints](https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/5177746fbb4edb28b6ba09137247d142af575c47/media/waypoints.webp)
## Difficult Terrain
**To use support for difficult terrain you must install the [Terrain Ruler module](https://foundryvtt.com/packages/terrain-ruler)**
With the Terrain Ruler module installed, Drag Ruler is able to take difficult terrain that was placed with the [TerrainLayer module](https://foundryvtt.com/packages/TerrainLayer/) into account when measuring distances.
![Demonstration of Difficult Terrain support](https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/bb8ac5d1acb9d6374d06f7e9c8e2c2dd61192d56/media/terrain_layer.webp)
## Why would I want to use this instead of ShowDragDistance?
ShowDragDistance isn't maintained anymore. This means that it is at risk to stop working with every foundry update. In fact this process has already begun. As of Foundry Version 0.7.9 ShowDragDistance doesn't work anymore on gridless maps. Drag Ruler on the other hand is fully compatible with the current Foundry release and I'll continue updating it for future foundry releases for the forseeable future.
@@ -29,12 +36,17 @@ The game systems that offer Drag Ruler integration are:
- Cypher System (starting with version 1.13.0)
- Pathfinder 1 (starting with version 0.77.3)
- Pathfinder 2e (via the module [PF2E Drag Ruler Integration](https://foundryvtt.com/packages/pf2e-dragruler/))
- Tagmar RPG (starting with version 1.1.4)
- Tormenta20 (starting with version 1.1.37)
- Shadow of the Demon Lord (starting with version 1.7.15)
- WWII:OWB (starting with version 1.0.4)
## Translations
Drag Ruler is available in the follwing languages:
- Chinese (thanks to zeteticl)
- English
- German
- Japanese (thanks to touge)
## API
+83
View File
@@ -0,0 +1,83 @@
{
"drag-ruler": {
"dependencies": {
"ok": "OK",
"terrain-ruler": {
"title": "Wie man schwieriges Gelände mit Drag Ruler misst",
"text": "Du hast das {moduleName} Modul installiert. Drag Ruler kann schwieriges Gelände, das mit diesem Modul platziert wurde in seinen Messungen berücksichtigen. Hierzu greift Drag Ruler auf die Funktionalität von Terrain Ruler zurück. Wenn Terrain Ruler installiert und aktiviert ist wird Drag Ruler automatisch anfangen schwiriges Gelände in den Messungen zu berücksichtigen.",
"neverShowAgain": "Zeige dies nie wieder an"
},
"socketlib": {
"title": "Socketlib Modul fehlt",
"text": "Drag Ruler benötigt das socketlib-Modul, um korrekt zu funktionieren. Bitte aktiviere das socketlib-Modul in dieser Welt."
}
},
"resetMovementHistory": "Bewegungsverlauf zurücksetzen",
"genericSpeedProvider": {
"settings": {
"dashMultiplier": {
"name": "Sprint Multiplikator",
"hint": "Diese Einstellung kann genutzt werden, um eine zweite Geschwindigkeitsfarbe im gemessenen Pfad zu erhalten. Die zweite Farbe wird deaktiviert, wenn diese Einstellung auf 0 gesetzt ist."
},
"speedAttribute": {
"name": "Geschwindigkeitsattribut",
"hint": "Das Attribut, das die Gehgeschwindigkeit einer Spielfigur definiert. Diese Attribut wird während der Färbung des hervorgehobenen Pfads verwendet."
}
},
"speeds": {
"walk": "gehen",
"dash": "sprinten"
}
},
"settings": {
"alwaysShowSpeedForPCs": {
"name": "Geschwindigkeit von Spielercharakteren für jeden anzeigen",
"hint": "Wenn diese Einstellung aktiviert ist wird die Färbung der hervorgehobenen Felder bei Spielercharakteren allen Spielern angezeigt, selbst wenn diese keinen Zugriff auf den Charakterbogen haben."
},
"enableMovementHistory": {
"name": "Bewegungsverlauf während des Kampfes aktivieren",
"hint": "Wenn diesen Option aktiviert ist merkt sich Drag Ruler die Bewegungen, die während einer Runde eines Kampfes gemacht wurden und zeigt diese an, wenn das Token weiterbewegt wird."
},
"showGMRulerToPlayers": {
"name": "Lineal des Sipelleiters bei Spielern anzeigen",
"hint": "Wenn diese Option deaktiviert ist wird das Lineal des Spielleiters bei den anderen Spielern nicht angezeigt."
},
"speedProviderSettings": {
"name": "Einstellungen zur Spielsystemintegration",
"hint": "In diesem Menü befinden sich alle spielsystemspezifischen Einstellungen.",
"button": "Einstellungen zur Spielsystemintegration",
"windowTitle": "Einstellungen zur Spielsystemintegration",
"headers": {
"speedProvider": "Spielsystemintegration",
"speedProviderSettings": "Integrationsspezifische Einstellungen"
},
"activeProvider": {
"name": "Verwendete Spielsystemintegration",
"hint": "Die unten angezeigten Einstellungen hängen von der verwendeten Spielsystemintegration ab. Wenn der Spielleiter eine andere Spielsystemintegration auswählt können sich die zur Verfügung stehnden Einstellungen ändern."
},
"noSettings": "Diese Spielsystemintegration hat keine Einstellmöglichkeiten",
"color": {
"name": "Farbe für {colorName}",
"hint": "Die Farbe mit der Felder in {colorName} Reichweite hervorgehoben werden",
"unreachable": {
"name": "unerreichbar",
"hint": "Die Farbe für Felder, die von der bewegten Spielfigur nicht erreicht werden können"
}
},
"speedProvider": {
"name": "Verwendete Spielsystemintegration",
"hint": "Wähle aus, welche Spielsystemintegration für die farbliche Hervorhebung der Felder zuständig sein soll. Drag Ruler stellt eine generische Spielsystemintegration zur Verfügung, die grundlegende Funktionalität bietet und bei korrekter konfiguration mit allen Spielsystemen funktionieren sollte. Weitere Spielsystemintegrationen können durch Module und Spielsysteme angeboten werden. Eine andere Spielsystemintegration als die generische auszuwählen kann eine bessere Inegration in das verwendete Spielsystem bieten. Die Optionen unterhalb sind davon abhäng, welche Spielsystemintegration hier ausgewählt wurde.",
"choices": {
"module": "Modul {name}",
"native": "Generisch",
"system": "System {name}"
}
}
},
"swapSpacebarRightClick": {
"name": "Leertaste und Rechtsklick tauschen",
"hint": "Die Funktionen der Leertaste und des Rechtsklicks sind während eine Spielfigur bewegt wird vertauscht. Wenn diese Option aktiviert wrid können mit Rechtsklick Wegpunkte gesetzt werden und mit der Leertaste werden sie wieder gelöscht."
}
}
}
}
+21
View File
@@ -1,5 +1,18 @@
{
"drag-ruler": {
"dependencies": {
"ok": "OK",
"terrain-ruler": {
"title": "How to measure difficult terrain with Drag Ruler",
"text": "You have the {moduleName} module enabled. Drag Ruler is able to measure difficult terrain that was placed down using that module. To make this possible Drag Ruler utilizes the Terrain Ruler module. If Terrain Ruler is installed and activated Drag Ruler will automatically start to respect difficult terrain in it's measurements.",
"neverShowAgain": "Never show this again"
},
"socketlib": {
"title": "Socketlib Module Missing",
"text": "Drag Ruler requires the socketlib module to work properly. Please activate the socketlib module in this world."
}
},
"resetMovementHistory": "Reset Movement History",
"genericSpeedProvider": {
"settings": {
"dashMultiplier": {
@@ -21,6 +34,14 @@
"name": "Show PC speed to everyone",
"hint": "If enabled the coloring based on actor speed for player characters will shown to everyone, even if they don't have observer permission for the character sheet."
},
"enableMovementHistory": {
"name": "Enable movement history during combat",
"hint": "If enabled, Drag Ruler will remember the path a token took during it's turn in combat and will display it when you pick the token back up."
},
"showGMRulerToPlayers": {
"name": "Show GM ruler to players",
"hint": "If disabled the ruler of GMs will not be shown for non-GM players"
},
"speedProviderSettings": {
"name": "Speed Provider Settings",
"hint": "The Speed Provider Settings contain all the game system specific settings.",
+4
View File
@@ -21,6 +21,10 @@
"name": "プレイヤーキャラクターの移動速度を全員に公開する",
"hint": "有効にすると、アクターの観察者(Observer)権限を持たない人にも,移動範囲の色分けが表示されるようになります。"
},
"showGMRulerToPlayers": {
"name": "GMのルーラーをプレイヤーに見せる",
"hint": "無効の場合、GMのルーラーはプレイヤーに表示されません。"
},
"speedProviderSettings": {
"name": "移動速度の提供元",
"hint": "ゲームシステムに固有のすべての設定をここで行います。",
+66
View File
@@ -0,0 +1,66 @@
{
"drag-ruler": {
"genericSpeedProvider": {
"settings": {
"dashMultiplier": {
"name": "快步 倍數",
"hint": "這可用於在對測量路徑進行著色的過程中為棋子賦予第二速度。將其設定為0可禁用第二速度。"
},
"speedAttribute": {
"name": "速度屬性",
"hint": "定義棋子的行走速度的屬性。在對測量路徑進行著色時使用。"
}
},
"speeds": {
"walk": "走路",
"dash": "快步"
}
},
"settings": {
"alwaysShowSpeedForPCs": {
"name": "向所有人顯示PC速度",
"hint": "如果啟用, 玩家的角色(actor)的速度將顯示給所有人,即使他們沒有角色表的觀察者(Observer)權限也是如此。"
},
"showGMRulerToPlayers": {
"name": "向玩家顯示GM標尺",
"hint": "如果禁用,則不為非GM玩家顯示GM的標尺"
},
"speedProviderSettings": {
"name": "速度提供者設定",
"hint": "速度提供者設定 包括 所有遊戲系統特定的設定。",
"button": "速度提供者設定",
"windowTitle": "速度提供者設定",
"headers": {
"speedProvider": "速度提供者設定",
"speedProviderSettings": "特定於速度提供者的設定"
},
"activeProvider": {
"name": "當前活動的速度提供者",
"hint": "下面顯示的設定 取決於活動的 速度提供者。如果GM選擇其他速度提供者,則可用的設定可能會更改。"
},
"noSettings": "該速度提供者不提供任何配置選項。",
"color": {
"name": "{colorName}的顏色",
"hint": "將用於為{colorName}範圍內的正方形上色的顏色",
"unreachable": {
"name": "無法到達",
"hint": "指定拖動棋子無法到達的範圍的顏色"
}
},
"speedProvider": {
"name": "速度設定的提供者",
"hint": "選擇在著色期間為棋子提供速度信息的人。DragRuler提供了一個通用速度提供程序, 該提供程序提供了基本功能, 並且如果配置正確,則可以在所有遊戲系統上使用。可以通過遊戲系統和已安裝的Mod來提供更多速度提供程序。選擇與 標準速度提供者 不同的速度提供者可以更好地集成到您所使用的遊戲系統規則中。以下選項取決於此處選擇的速度提供者。",
"choices": {
"module": "Mod {name}",
"native": "一般",
"system": "系統 {name}"
}
}
},
"swapSpacebarRightClick": {
"name": "交換空格鍵並鼠標右鍵",
"hint": "交換空格鍵和右鍵點擊在拖動過程中的功能。如果啟用,右鍵單擊將放置航點,空格鍵將刪除它們。"
}
}
}
}
+23 -4
View File
@@ -2,7 +2,7 @@
"name": "drag-ruler",
"title": "Drag Ruler",
"description": "When dragging a token displays a ruler showing how far you've moved that token.",
"version": "1.3.3",
"version": "1.6.5",
"minimumCoreVersion" : "0.7.9",
"compatibleCoreVersion" : "0.7.9",
"authors": [
@@ -13,12 +13,18 @@
}
],
"esmodules": [
"src/main.js"
"src/main.js",
"src/socket.js"
],
"templates": [
"speed_provider_settings.html"
],
"languages": [
{
"lang": "de",
"name": "Deutsch",
"path": "lang/de.json"
},
{
"lang": "en",
"name": "English",
@@ -28,12 +34,25 @@
"lang": "ja",
"name": "日本語",
"path": "lang/ja.json"
},
{
"lang": "zh-tw",
"name": "正體中文",
"path": "lang/zh-tw.json"
}
],
"dependencies": [
{
"name": "socketlib",
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-socketlib/master/module.json"
}
],
"socket": true,
"url": "https://github.com/manuelVo/foundryvtt-drag-ruler",
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/v1.3.3.zip",
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/v1.6.5.zip",
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/master/module.json",
"readme": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/README.md",
"changelog": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/CHANGELOG.md",
"bugs": "https://github.com/manuelVo/foundryvtt-drag-ruler/issues"
"bugs": "https://github.com/manuelVo/foundryvtt-drag-ruler/issues",
"allowBugReporter": true
}
+25
View File
@@ -1,5 +1,8 @@
import {measureDistances} from "./compatibility.js";
import {getMovementHistory} from "./movement_tracking.js";
import {GenericSpeedProvider, SpeedProvider} from "./speed_provider.js"
import {settingsKey} from "./settings.js"
import {getTokenShape} from "./util.js";
export const availableSpeedProviders = {}
export let currentSpeedProvider = undefined
@@ -97,6 +100,28 @@ export function getUnreachableColorFromSpeedProvider() {
}
}
export function getCostFromSpeedProvider(token, area) {
try {
if (currentSpeedProvider instanceof Function) {
return SpeedProvider.prototype.getCostForStep.call(undefined, token, area);
}
return currentSpeedProvider.getCostForStep(token, area);
}
catch (e) {
console.error(e);
return 1;
}
}
export function getMovedDistanceFromToken(token) {
const history = getMovementHistory(token);
const segments = Ruler.dragRulerGetRaysFromWaypoints(history, {x: token.x, y: token.y}).map(ray => {return {ray}});
const shape = getTokenShape(token);
const distances = measureDistances(segments, token, shape);
// Sum up the distances
return distances.reduce((acc, val) => acc + val, 0);
}
export function registerModule(moduleId, speedProvider) {
// Check if a module with the given id exists and is currently enabled
const module = game.modules.get(moduleId)
+80 -13
View File
@@ -1,19 +1,86 @@
import {getCostFromSpeedProvider} from "./api.js";
import {getColorForDistance} from "./main.js"
import {settingsKey} from "./settings.js";
import {getAreaFromPositionAndShape, highlightTokenShape} from "./util.js";
export function getHexSizeSupportTokenGridCenter(token) {
const tokenCenterOffset = CONFIG.hexSizeSupport.getCenterOffset(token)
const tokenCenter = {x: token.x + tokenCenterOffset.x, y: token.y + tokenCenterOffset.y}
if (token.getFlag("hex-size-support", "borderSize") % 2 === 1)
return tokenCenter
if (canvas.grid.grid.columns) {
let hexOffset = canvas.grid.w / 2
if (!CONFIG.hexSizeSupport.getAltOrientationFlag(token))
hexOffset *= -1
tokenCenter.x += hexOffset
return {x: token.x + tokenCenterOffset.x, y: token.y + tokenCenterOffset.y}
}
export function highlightMeasurementTerrainRuler(ray, startDistance, tokenShape=[{x: 0, y: 0}], alpha=1) {
for (const space of ray.terrainRulerVisitedSpaces.reverse()) {
const color = getColorForDistance.call(this, startDistance, space.distance)
highlightTokenShape.call(this, space, tokenShape, color, alpha)
}
}
export function measureDistances(segments, token, shape, gridSpaces=true, options={}) {
const opts = duplicate(options)
opts.gridSpaces = gridSpaces;
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS;
if (terrainRulerAvailable) {
const firstNewSegmentIndex = segments.findIndex(segment => !segment.ray.dragRulerVisitedSpaces);
const previousSegments = segments.slice(0, firstNewSegmentIndex);
const newSegments = segments.slice(firstNewSegmentIndex);
const distances = previousSegments.map(segment => segment.ray.dragRulerVisitedSpaces[segment.ray.dragRulerVisitedSpaces.length - 1].distance);
previousSegments.forEach(segment => segment.ray.terrainRulerVisitedSpaces = duplicate(segment.ray.dragRulerVisitedSpaces));
opts.costFunction = (x, y) => getCostFromSpeedProvider(token, getAreaFromPositionAndShape({x, y}, shape), {x, y});
if (previousSegments.length > 0)
opts.terrainRulerInitialState = previousSegments[previousSegments.length - 1];
return distances.concat(terrainRuler.measureDistances(newSegments, opts));
}
else {
let hexOffset = canvas.grid.h / 2
if (CONFIG.hexSizeSupport.getAltOrientationFlag(token))
hexOffset *= -1
tokenCenter.y += hexOffset
return canvas.grid.measureDistances(segments, opts);
}
return tokenCenter
}
export function checkDependencies() {
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.");
if (game.user.isGM) {
new Dialog({
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>`,
buttons: {
ok: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize("drag-ruler.dependencies.ok")
}
},
}).render(true);
}
}
else if (!game.modules.get("terrain-ruler")?.active && game.user.isGM && !game.settings.get(settingsKey, "neverShowTerrainRulerHint")) {
const lastHint = game.settings.get(settingsKey, "lastTerrainRulerHintTime");
if (Date.now() - lastHint > 604800000) { // One week
let enabledTerrainModule;
if (game.modules.get("enhanced-terrain-layer")?.active) {
enabledTerrainModule = game.modules.get("enhanced-terrain-layer").data.title;
}
else if (game.modules.get("TerrainLayer")?.active) {
enabledTerrainModule = game.modules.get("TerrainLayer").data.title;
}
if (enabledTerrainModule) {
new Dialog({
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>`,
buttons: {
ok: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize("drag-ruler.dependencies.ok"),
callback: () => game.settings.set(settingsKey, "lastTerrainRulerHintTime", Date.now()),
},
neverShowAgain: {
icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize("drag-ruler.dependencies.terrain-ruler.neverShowAgain"),
callback: () => game.settings.set(settingsKey, "neverShowTerrainRulerHint", true),
}
},
close: () => game.settings.set(settingsKey, "lastTerrainRulerHintTime", Date.now())
}).render(true);
}
}
}
}
+18
View File
@@ -0,0 +1,18 @@
// 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
export function getPixelsFromGridPosition(xGrid, yGrid) {
if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS) {
return canvas.grid.grid.getPixelsFromGridPosition(yGrid, xGrid)
}
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
// https://gitlab.com/foundrynet/foundryvtt/-/issues/4705
export function getGridPositionFromPixels(xPixel, yPixel) {
const [x, y] = canvas.grid.grid.getGridPositionFromPixels(xPixel, yPixel)
if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS)
return [y, x]
return [x, y]
}
+130 -41
View File
@@ -1,4 +1,9 @@
import {highlightMeasurementTerrainRuler, measureDistances} from "./compatibility.js";
import {getGridPositionFromPixels} from "./foundry_fixes.js";
import {getColorForDistance} from "./main.js"
import {trackRays} from "./movement_tracking.js"
import {recalculate} from "./socket.js";
import {applyTokenSizeOffset, getSnapPointForToken, getTokenShape, highlightTokenShape, zip} from "./util.js";
// This is a modified version of Ruler.moveToken from foundry 0.7.9
export async function moveTokens(draggedToken, selectedTokens) {
@@ -12,11 +17,11 @@ export async function moveTokens(draggedToken, selectedTokens) {
// Get the movement rays and check collision along each Ray
// These rays are center-to-center for the purposes of collision checking
const rays = this._getRaysFromWaypoints(this.waypoints, this.destination);
const rays = this.constructor.dragRulerGetRaysFromWaypoints(this.waypoints, this.destination);
if (!game.user.isGM) {
const hasCollision = selectedTokens.some(token => {
const offset = calculateTokenOffset(token, draggedToken)
const offsetRays = rays.map(ray => applyOffsetToRay(ray, offset))
const offsetRays = rays.filter(ray => !ray.isPrevious).map(ray => applyOffsetToRay(ray, offset))
return offsetRays.some(r => canvas.walls.checkCollision(r));
})
if (hasCollision) {
@@ -29,42 +34,61 @@ export async function moveTokens(draggedToken, selectedTokens) {
// Execute the movement path.
// Transform each center-to-center ray into a top-left to top-left ray using the prior token offsets.
this._state = Ruler.STATES.MOVING;
await Promise.all(selectedTokens.map(token => {
// Return the promise so we can wait for it outside the loop
const offset = calculateTokenOffset(token, draggedToken)
return animateToken.call(this, token, rays, offset, wasPaused)
}))
await animateTokens.call(this, selectedTokens, draggedToken, rays, wasPaused);
// Once all animations are complete we can clear the ruler
this._endMeasurement();
if (this.draggedToken?.id === draggedToken.id)
this._endMeasurement();
}
// This is a modified version code extracted from Ruler.moveToken from foundry 0.7.9
async function animateToken(token, rays, tokenOffset, wasPaused) {
const offsetRays = rays.map(ray => applyOffsetToRay(ray, tokenOffset))
async function animateTokens(tokens, draggedToken, draggedRays, wasPaused) {
const newRays = draggedRays.filter(r => !r.isPrevious);
const tokenAnimationData = tokens.map(token => {
const tokenOffset = calculateTokenOffset(token, draggedToken);
const offsetRays = newRays.map(ray => applyOffsetToRay(ray, tokenOffset));
// Determine offset relative to the Token top-left.
// This is important so we can position the token relative to the ruler origin for non-1x1 tokens.
const origin = [this.waypoints[0].x + tokenOffset.x, this.waypoints[0].y + tokenOffset.y]
let dx, dy
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
dx = token.data.x - origin[0]
dy = token.data.y - origin[1]
}
else {
dx = token.data.x - origin[0]
dy = token.data.y - origin[1]
}
// Determine offset relative to the Token top-left.
// This is important so we can position the token relative to the ruler origin for non-1x1 tokens.
const firstWaypoint = this.waypoints.find(w => !w.isPrevious);
const origin = [firstWaypoint.x + tokenOffset.x, firstWaypoint.y + tokenOffset.y];
let dx, dy;
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
dx = token.data.x - origin[0];
dy = token.data.y - origin[1];
}
else {
dx = token.data.x - origin[0];
dy = token.data.y - origin[1];
}
token._noAnimate = true;
for (let r of offsetRays) {
return {token, rays: offsetRays, dx, dy};
});
for (const {token, rays} of tokenAnimationData) {
token._noAnimate = true;
}
const animate = !game.keyboard.isDown("Alt");
const startWaypoint = animate ? 0 : tokenAnimationData[0].rays.length - 1;
for (let i = startWaypoint;i < tokenAnimationData[0].rays.length; i++) {
if (!wasPaused && game.paused) break;
const dest = [r.B.x, r.B.y];
const path = new Ray({ x: token.x, y: token.y }, { x: dest[0] + dx, y: dest[1] + dy });
await token.update(path.B);
await token.animateMovement(path);
const tokenPaths = tokenAnimationData.map(({token, rays, dx, dy}) => {
const ray = rays[i];
const dest = [ray.B.x, ray.B.y];
const path = new Ray({x: token.x, y: token.y}, {x: dest[0] + dx, y: dest[1] + dy});
return {token, path};
});
const updates = tokenPaths.map(({token, path}) => {
return {x: path.B.x, y: path.B.y, _id: token.id};
});
await draggedToken.scene.updateEmbeddedEntity(draggedToken.constructor.embeddedName, updates, {animate});
if (animate)
await Promise.all(tokenPaths.map(({token, path}) => token.animateMovement(path)));
}
token._noAnimate = false;
for (const {token} of tokenAnimationData) {
token._noAnimate = false;
}
trackRays(tokens, tokenAnimationData.map(({rays}) => rays)).then(() => recalculate(tokens));
}
function calculateTokenOffset(tokenA, tokenB) {
@@ -72,7 +96,9 @@ function calculateTokenOffset(tokenA, tokenB) {
}
function applyOffsetToRay(ray, offset) {
return 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;
return newRay;
}
// This is a modified version of Ruler._onMouseMove from foundry 0.7.9
@@ -102,9 +128,17 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) {
return []
if (snap)
destination = new PIXI.Point(...canvas.grid.getCenter(destination.x, destination.y));
destination = getSnapPointForToken(destination.x, destination.y, this.draggedToken)
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS;
const waypoints = this.waypoints.concat([destination]);
const centeredWaypoints = waypoints.map(w => new PIXI.Point(...canvas.grid.getCenter(w.x, w.y)))
// Move the waypoints to the center of the grid if a size is used that measures from edge to edge
const centeredWaypoints = applyTokenSizeOffset(waypoints, this.draggedToken)
// Foundries native ruler requires the waypoints to sit in the dead center of the square to work properly
if (!terrainRulerAvailable)
centeredWaypoints.forEach(w => [w.x, w.y] = canvas.grid.getCenter(w.x, w.y));
const r = this.ruler;
this.destination = destination;
@@ -118,6 +152,10 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) {
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;
if (ray.distance < 10) {
if (label) label.visible = false;
continue;
@@ -126,14 +164,18 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) {
centeredSegments.push({ray: centeredRay, label})
}
const shape = getTokenShape(this.draggedToken)
// Compute measured distance
const distances = canvas.grid.measureDistances(centeredSegments, { gridSpaces });
const distances = measureDistances(centeredSegments, this.draggedToken, shape, gridSpaces);
let totalDistance = 0;
for (let [i, d] of distances.entries()) {
let s = segments[i];
let s = centeredSegments[i];
s.startDistance = totalDistance
totalDistance += d;
s.last = i === (segments.length - 1);
s.last = i === (centeredSegments.length - 1);
s.distance = d;
s.text = this._getSegmentLabel(d, totalDistance, s.last);
}
@@ -149,24 +191,28 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) {
rulerColor = getColorForDistance.call(this, totalDistance)
else
rulerColor = this.color
for (let s of segments) {
const { ray, label, text, last } = s;
for (const [s, cs] of zip(segments.reverse(), centeredSegments.reverse())) {
const { label, text, last } = cs;
// Draw line segment
r.lineStyle(6, 0x000000, 0.5).moveTo(ray.A.x, ray.A.y).lineTo(ray.B.x, ray.B.y)
.lineStyle(4, rulerColor, 0.25).moveTo(ray.A.x, ray.A.y).lineTo(ray.B.x, ray.B.y);
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 = ray.project((ray.distance + 50) / ray.distance);
let labelPosition = cs.ray.project((cs.ray.distance + 50) / cs.ray.distance);
label.position.set(labelPosition.x, labelPosition.y);
}
// Highlight grid positions
this._highlightMeasurement(ray, s.startDistance);
if (terrainRulerAvailable)
highlightMeasurementTerrainRuler.call(this, cs.ray, cs.startDistance, shape, opacityMultiplier)
else
highlightMeasurementNative.call(this, cs.ray, cs.startDistance, shape, opacityMultiplier);
}
// Draw endpoints
@@ -177,3 +223,46 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) {
// Return the measured segments
return segments;
}
export function highlightMeasurementNative(ray, startDistance, tokenShape=[{x: 0, y: 0}], alpha=1) {
const spacer = canvas.scene.data.gridType === CONST.GRID_TYPES.SQUARE ? 1.41 : 1;
const nMax = Math.max(Math.floor(ray.distance / (spacer * Math.min(canvas.grid.w, canvas.grid.h))), 1);
const tMax = Array.fromRange(nMax+1).map(t => t / nMax);
// Track prior position
let prior = null;
// Iterate over ray portions
for ( let [i, t] of tMax.reverse().entries() ) {
let {x, y} = ray.project(t);
// Get grid position
let [x0, y0] = (i === 0) ? [null, null] : prior;
let [x1, y1] = canvas.grid.grid.getGridPositionFromPixels(x, y);
if ( x0 === x1 && y0 === y1 ) continue;
// Highlight the grid position
let [xg, yg] = canvas.grid.grid.getPixelsFromGridPosition(x1, y1);
const subDistance = canvas.grid.measureDistances([{ray: new Ray(ray.A, {x: xg, y: yg})}], {gridSpaces: true})[0]
const color = dragRuler.getColorForDistance.call(this, startDistance, subDistance)
const snapPoint = getSnapPointForToken(...canvas.grid.getTopLeft(x, y), this.draggedToken);
const [snapX, snapY] = getGridPositionFromPixels(snapPoint.x + 1, snapPoint.y + 1);
prior = [x1, y1];
// If the positions are not neighbors, also highlight their halfway point
if (i > 0 && !canvas.grid.isNeighbor(x0, y0, x1, y1)) {
let th = tMax[i - 1] - (0.5 / nMax);
let {x, y} = ray.project(th);
let [x1h, y1h] = canvas.grid.grid.getGridPositionFromPixels(x, y);
let [xgh, ygh] = canvas.grid.grid.getPixelsFromGridPosition(x1h, y1h);
const subDistance = canvas.grid.measureDistances([{ray: new Ray(ray.A, {x: xgh, y: ygh})}], {gridSpaces: true})[0]
const color = dragRuler.getColorForDistance.call(this, startDistance, subDistance)
const snapPoint = getSnapPointForToken(...canvas.grid.getTopLeft(x, y), this.draggedToken);
const [snapX, snapY] = getGridPositionFromPixels(snapPoint.x + 1, snapPoint.y + 1);
highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color, alpha);
}
highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color, alpha);
}
}
+42 -121
View File
@@ -1,29 +1,36 @@
"use strict"
import {currentSpeedProvider, getRangesFromSpeedProvider, getUnreachableColorFromSpeedProvider, initApi, registerModule, registerSystem} from "./api.js"
import {getHexSizeSupportTokenGridCenter} from "./compatibility.js"
import {measure, moveTokens, onMouseMove} from "./foundry_imports.js"
import {currentSpeedProvider, getMovedDistanceFromToken, getRangesFromSpeedProvider, getUnreachableColorFromSpeedProvider, initApi, registerModule, registerSystem} from "./api.js"
import {checkDependencies, getHexSizeSupportTokenGridCenter} from "./compatibility.js";
import {moveTokens, onMouseMove} from "./foundry_imports.js"
import {performMigrations} from "./migration.js"
import {DragRulerRuler} from "./ruler.js";
import {getMovementHistory, resetMovementHistory} from "./movement_tracking.js";
import {registerSettings, settingsKey} from "./settings.js"
import {recalculate} from "./socket.js";
import {SpeedProvider} from "./speed_provider.js"
Hooks.once("init", () => {
registerSettings()
initApi()
hookTokenDragHandlers()
hookRulerFunctions()
hookKeyboardManagerFunctions()
patchRulerHighlightMeasurement()
Ruler = DragRulerRuler;
window.dragRuler = {
getColorForDistance,
getMovedDistanceFromToken,
registerModule,
registerSystem,
recalculate,
resetMovementHistory,
}
})
Hooks.once("ready", () => {
performMigrations()
checkDependencies();
Hooks.callAll("dragRuler.ready", SpeedProvider)
})
@@ -38,6 +45,15 @@ Hooks.on("canvasReady", () => {
})
})
Hooks.on("getCombatTrackerEntryContext", function (html, menu) {
const entry = {
name: "drag-ruler.resetMovementHistory",
icon: '<i class="fas fa-undo-alt"></i>',
callback: li => resetMovementHistory(ui.combat.combat, li.data('combatant-id')),
};
menu.splice(1, 0, entry);
});
function hookTokenDragHandlers() {
const originalDragLeftStartHandler = Token.prototype._onDragLeftStart
Token.prototype._onDragLeftStart = function(event) {
@@ -66,42 +82,6 @@ function hookTokenDragHandlers() {
}
}
function hookRulerFunctions() {
const originalMoveTokenHandler = Ruler.prototype.moveToken
Ruler.prototype.moveToken = function (event) {
const eventHandled = onRulerMoveToken.call(this, event)
if (!eventHandled)
return originalMoveTokenHandler.call(this, event)
return true
}
const originalToJSON = Ruler.prototype.toJSON
Ruler.prototype.toJSON = function () {
const json = originalToJSON.call(this)
if (this.draggedToken)
json["draggedToken"] = this.draggedToken.data._id
return json
}
const originalUpdate = Ruler.prototype.update
Ruler.prototype.update = function (data) {
if (data.draggedToken) {
this.draggedToken = canvas.tokens.get(data.draggedToken)
}
originalUpdate.call(this, data)
}
const originalMeasure = Ruler.prototype.measure
Ruler.prototype.measure = function (destination, options={}) {
if (this.isDragRuler) {
return measure.call(this, destination, options)
}
else {
return originalMeasure.call(this, destination, options)
}
}
}
function hookKeyboardManagerFunctions() {
const originalHandleKeys = KeyboardManager.prototype._handleKeys
KeyboardManager.prototype._handleKeys = function (event, key, up) {
@@ -123,10 +103,11 @@ function handleKeys(event, key, up) {
function onKeyX(up) {
if (up)
return false
if (!canvas.controls.ruler.isDragRuler)
const ruler = canvas.controls.ruler;
if (!ruler.isDragRuler)
return false
deleteWaypoint()
ruler.dragRulerDeleteWaypoint();
return true
}
@@ -150,11 +131,13 @@ function onTokenLeftDragStart(event) {
if (canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(this))
tokenCenter = getHexSizeSupportTokenGridCenter(this)
else
tokenCenter = {x: this.x + canvas.grid.grid.w / 2, y: this.y + canvas.grid.grid.h / 2}
tokenCenter = this.center
ruler.clear();
ruler._state = Ruler.STATES.STARTING;
ruler.rulerOffset = {x: tokenCenter.x - event.data.origin.x, y: tokenCenter.y - event.data.origin.y}
addWaypoint.call(ruler, tokenCenter, false)
if (game.settings.get(settingsKey, "enableMovementHistory"))
ruler.dragRulerAddWaypointHistory(getMovementHistory(this));
ruler.dragRulerAddWaypoint(tokenCenter, false);
}
function onTokenLeftDragMove(event) {
@@ -167,85 +150,40 @@ function onTokenDragLeftDrop(event) {
const ruler = canvas.controls.ruler
if (!ruler.isDragRuler)
return false
onMouseMove.call(ruler, event);
const selectedTokens = canvas.tokens.controlled
// This can happen if the user presses ESC during drag (maybe there are other ways too)
if (selectedTokens.length === 0)
selectedTokens.push(ruler.draggedToken);
ruler._state = Ruler.STATES.MOVING
moveTokens.call(ruler, ruler.draggedToken, selectedTokens)
ruler.draggedToken = null
return true
}
function onTokenDragLeftCancel(event) {
// This function is invoked by right clicking
const ruler = canvas.controls.ruler
if (!ruler.isDragRuler)
if (!ruler.isDragRuler || ruler._state === Ruler.STATES.MOVING)
return false
if (ruler._state === Ruler.STATES.MEASURING) {
if (!game.settings.get(settingsKey, "swapSpacebarRightClick")) {
if (ruler.waypoints.length > 1)
event.preventDefault()
deleteWaypoint()
ruler.dragRulerDeleteWaypoint(event);
}
else {
event.preventDefault()
const snap = !event.shiftKey
addWaypoint.call(ruler, ruler.destination, snap)
ruler.dragRulerAddWaypoint(ruler.destination, snap);
}
}
return true
}
function onRulerMoveToken(event) {
// This function is invoked by left clicking
if (!this.isDragRuler)
return false
if (!game.settings.get(settingsKey, "swapSpacebarRightClick")) {
const snap = !event.shiftKey
addWaypoint.call(this, this.destination, snap)
}
else
deleteWaypoint()
return true
}
function addWaypoint(point, snap=true) {
if (snap)
point = canvas.grid.getCenter(point.x, point.y);
else
point = [point.x, point.y]
this.waypoints.push(new PIXI.Point(point[0], point[1]));
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
}
function deleteWaypoint() {
const ruler = canvas.controls.ruler
if (ruler.waypoints.length > 1) {
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens)
const rulerOffset = ruler.rulerOffset
ruler._removeWaypoint({x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y})
game.user.broadcastActivity({ruler: ruler})
}
else {
const token = ruler.draggedToken
ruler._endMeasurement()
ruler.draggedToken = null
// Deactivate the drag workflow in mouse
token.mouseInteractionManager._deactivateDragEvents();
token.mouseInteractionManager.state = token.mouseInteractionManager.states.HOVER;
// This will cancel the current drag operation
// Pass in a fake event that hopefully is enough to allow other modules to function
token._onDragLeftCancel({preventDefault: () => {return}})
}
}
function strInsertAfter(haystack, needle, strToInsert) {
const pos = haystack.indexOf(needle) + needle.length
return haystack.slice(0, pos) + strToInsert + haystack.slice(pos)
}
export function getColorForDistance(startDistance, subDistance=0) {
if (!this.isDragRuler)
return this.color
if (!this.draggedToken.actor) {
return this.color;
}
// Don't apply colors if the current user doesn't have at least observer permissions
if (this.draggedToken.actor.permission < 2) {
// If this is a pc and alwaysShowSpeedForPCs is enabled we show the color anyway
@@ -253,7 +191,9 @@ export function getColorForDistance(startDistance, subDistance=0) {
return this.color
}
const distance = startDistance + subDistance
const ranges = getRangesFromSpeedProvider(this.draggedToken)
if (!this.dragRulerRanges)
this.dragRulerRanges = getRangesFromSpeedProvider(this.draggedToken)
const ranges = this.dragRulerRanges;
if (ranges.length === 0)
return this.color
const currentRange = ranges.reduce((minRange, currentRange) => {
@@ -263,22 +203,3 @@ export function getColorForDistance(startDistance, subDistance=0) {
}, {range: Infinity, color: getUnreachableColorFromSpeedProvider()})
return currentRange.color
}
// These patches were written with foundry-0.7.9.js as reference
function patchRulerHighlightMeasurement() {
let code = Ruler.prototype._highlightMeasurement.toString()
// Replace CRLF with LF in case foundry.js has CRLF for some reason
code = code.replace(/\r\n/g, "\n")
// Remove function signature and closing curly bracket (those are on the first and last line)
code = code.slice(code.indexOf("\n"), code.lastIndexOf("\n"))
const calcColorCode = `
let subDistance = canvas.grid.measureDistances([{ray: new Ray(ray.A, {x: xg, y: yg})}], {gridSpaces: true})[0]
let color = dragRuler.getColorForDistance.call(this, startDistance, subDistance)
`
code = strInsertAfter(code, "Position(x1, y1);\n", calcColorCode)
code = strInsertAfter(code, "Position(x1h, y1h);\n", calcColorCode.replace("x: xg, y: yg", "x: xgh, y: ygh"))
code = code.replace(/color: this\.color\}/g, "color}")
Ruler.prototype._highlightMeasurement = new Function("ray", "startDistance=undefined", code)
}
+94
View File
@@ -0,0 +1,94 @@
import {measureDistances} from "./compatibility.js";
import {recalculate, updateCombatantDragRulerFlags} from "./socket.js";
import {getTokenShape, zip} from "./util.js";
function initTrackingFlag(combatant) {
const initialFlag = {passedWaypoints: [], trackedRound: 0};
let dragRulerFlag = combatant.flags?.dragRuler;
if (dragRulerFlag) {
if (isNaN(dragRulerFlag.trackedRound)) {
mergeObject(dragRulerFlag, initialFlag);
}
}
else {
combatant.flags.dragRuler = initialFlag;
}
}
function getInitializedCombatant(token, combat) {
const combatant = combat.getCombatantByToken(token.data._id);
if (!combatant)
return undefined;
initTrackingFlag(combatant);
return combatant;
}
export async function trackRays(tokens, tokenRays) {
const combat = game.combat;
if (!combat)
return;
if (!combat.started)
return;
if (!(tokens instanceof Array)) {
tokens = [tokens];
tokenRays = [tokenRays];
}
const updates = Array.from(zip(tokens, tokenRays)).map(([token, rays]) => calculateUpdate(combat, token, rays)).filter(Boolean);
await updateCombatantDragRulerFlags(combat, updates);
}
function calculateUpdate(combat, token, rays) {
const combatant = getInitializedCombatant(token, combat);
if (!combatant)
return;
// Check if we have entered a new round. If so, remove the currently stored path
if (combat.data.round > combatant.flags.dragRuler.trackedRound) {
combatant.flags.dragRuler.passedWaypoints = [];
combatant.flags.dragRuler.trackedRound = combat.data.round;
}
// Add the passed waypoints to the combatant
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS;
const dragRulerFlags = combatant.flags.dragRuler;
const waypoints = dragRulerFlags.passedWaypoints;
for (const ray of rays) {
// Ignore rays that have the same start and end coordinates
if (ray.A.x !== ray.B.x || ray.A.y !== ray.B.y) {
if (terrainRulerAvailable) {
measureDistances([{ray}], token, getTokenShape(token), true, {terrainRulerInitialState: dragRulerFlags.rulerState});
ray.A.dragRulerVisitedSpaces = ray.terrainRulerVisitedSpaces;
dragRulerFlags.rulerState = ray.terrainRulerFinalState;
}
waypoints.push(ray.A);
}
}
return {_id: combatant._id, dragRulerFlags};
}
export function getMovementHistory(token) {
const combat = game.combat;
if (!combat)
return [];
const combatant = combat.getCombatantByToken(token.data._id);
if (!combatant)
return [];
const dragRulerFlags = combatant.flags.dragRuler;
if (!dragRulerFlags)
return [];
if (combat.data.round > dragRulerFlags.trackedRound)
return [];
return dragRulerFlags.passedWaypoints ?? [];
}
export async function resetMovementHistory(combat, combatantId) {
const combatant = combat.getCombatant(combatantId);
const dragRulerFlags = combatant.flags.dragRuler;
if (!dragRulerFlags)
return;
dragRulerFlags.passedWaypoints = null;
dragRulerFlags.trackedRound = null;
dragRulerFlags.rulerState = null;
await updateCombatantDragRulerFlags(combat, [{_id: combatantId, dragRulerFlags}]);
recalculate();
}
+134
View File
@@ -0,0 +1,134 @@
import {measure} from "./foundry_imports.js"
import {getMovementHistory} from "./movement_tracking.js";
import {settingsKey} from "./settings.js";
import {getSnapPointForToken} from "./util.js";
export class DragRulerRuler extends Ruler {
// Functions below are overridden versions of functions in Ruler
constructor(user, {color=null}={}) {
super(user, {color});
this.previousWaypoints = [];
this.previousLabels = this.addChild(new PIXI.Container());
}
clear() {
super.clear();
this.previousWaypoints = [];
this.previousLabels.removeChildren().forEach(c => c.destroy());
this.dragRulerRanges = undefined;
}
async moveToken(event) {
// This function is invoked by left clicking
if (!this.isDragRuler)
return await super.moveToken(event);
if (!game.settings.get(settingsKey, "swapSpacebarRightClick")) {
const snap = !event.shiftKey;
this.dragRulerAddWaypoint(this.destination, snap);
}
else {
this.dragRulerDeleteWaypoint();
}
}
toJSON() {
const json = super.toJSON();
if (this.draggedToken)
json["draggedToken"] = this.draggedToken.data._id;
return json;
}
update(data) {
// Don't show a GMs drag ruler to non GM players
if (data.draggedToken && this.user.isGM && !game.user.isGM && !game.settings.get(settingsKey, "showGMRulerToPlayers"))
return;
if (data.draggedToken) {
this.draggedToken = canvas.tokens.get(data.draggedToken);
}
super.update(data);
}
measure(destination, options={}) {
if (this.isDragRuler) {
return measure.call(this, destination, options);
}
else {
return super.measure(destination, options);
}
}
_endMeasurement() {
super._endMeasurement();
this.draggedToken = null;
}
// The functions below aren't present in the orignal Ruler class and are added by Drag Ruler
dragRulerAddWaypoint(point, snap=true) {
if (snap)
point = getSnapPointForToken(point.x, point.y, this.draggedToken);
this.waypoints.push(new PIXI.Point(point.x, point.y));
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
}
dragRulerAddWaypointHistory(waypoints) {
waypoints.forEach(waypoint => waypoint.isPrevious = true);
this.waypoints = this.waypoints.concat(waypoints);
for (const waypoint of waypoints) {
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
}
}
dragRulerClearWaypoints() {
this.waypoints = [];
this.labels.removeChildren().forEach(c => c.destroy());
}
dragRulerDeleteWaypoint(event={preventDefault: () => {return}}) {
if (this.waypoints.filter(w => !w.isPrevious).length > 1) {
event.preventDefault();
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens);
const rulerOffset = this.rulerOffset;
this._removeWaypoint({x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y});
game.user.broadcastActivity({ruler: this});
}
else {
const token = this.draggedToken;
this._endMeasurement();
// Deactivate the drag workflow in mouse
token.mouseInteractionManager._deactivateDragEvents();
token.mouseInteractionManager.state = token.mouseInteractionManager.states.HOVER;
// This will cancel the current drag operation
// Pass in a fake event that hopefully is enough to allow other modules to function
token._onDragLeftCancel(event);
}
}
async dragRulerRecalculate(tokenIds) {
if (this._state !== Ruler.STATES.MEASURING)
return;
if (tokenIds && !tokenIds.includes(this.draggedToken.id))
return;
const waypoints = this.waypoints.filter(waypoint => !waypoint.isPrevious);
this.dragRulerClearWaypoints();
if (game.settings.get(settingsKey, "enableMovementHistory"))
this.dragRulerAddWaypointHistory(getMovementHistory(this.draggedToken));
for (const waypoint of waypoints) {
this.dragRulerAddWaypoint(waypoint, false);
}
this.measure(this.destination);
game.user.broadcastActivity({ruler: this});
}
static dragRulerGetRaysFromWaypoints(waypoints, destination) {
if ( destination )
waypoints = waypoints.concat([destination]);
return waypoints.slice(1).map((wp, i) => {
const ray = new Ray(waypoints[i], wp);
ray.isPrevious = Boolean(waypoints[i].isPrevious);
return ray;
});
}
}
+30
View File
@@ -29,6 +29,36 @@ export function registerSettings() {
default: true,
})
game.settings.register(settingsKey, "showGMRulerToPlayers", {
name: "drag-ruler.settings.showGMRulerToPlayers.name",
hint: "drag-ruler.settings.showGMRulerToPlayers.hint",
scope: "world",
config: true,
type: Boolean,
default: true,
})
game.settings.register(settingsKey, "enableMovementHistory", {
name: "drag-ruler.settings.enableMovementHistory.name",
hint: "drag-ruler.settings.enableMovementHistory.hint",
scope: "world",
config: true,
type: Boolean,
default: true,
});
game.settings.register(settingsKey, "lastTerrainRulerHintTime", {
config: false,
type: Number,
default: 0,
});
game.settings.register(settingsKey, "neverShowTerrainRulerHint", {
config: false,
type: Boolean,
default: false,
});
// This setting will be modified by the api if modules register to it
game.settings.register(settingsKey, "speedProvider", {
scope: "world",
+43
View File
@@ -0,0 +1,43 @@
import {currentSpeedProvider} from "./api.js";
let socket;
Hooks.once("socketlib.ready", () => {
socket = socketlib.registerModule("drag-ruler");
socket.register("updateCombatantDragRulerFlags", _socketUpdateCombatantDragRulerFlags);
socket.register("recalculate", _socketRecalculate);
});
export function updateCombatantDragRulerFlags(combat, updates) {
const combatId = combat.id;
// TODO Check if canvas.tokens.get is still neccessary in future foundry versions
return socket.executeAsGM(_socketUpdateCombatantDragRulerFlags, combatId, updates)
.then(() => currentSpeedProvider.onMovementHistoryUpdate(updates.map(update => canvas.tokens.get(combat.getCombatant(update._id).token._id))));
}
async function _socketUpdateCombatantDragRulerFlags(combatId, updates) {
const user = game.users.get(this.socketdata.userId);
const combat = game.combats.get(combatId);
const requestedUpdates = updates.length;
updates = updates.filter(update => {
const actor = combat.getCombatant(update._id).actor;
if (!actor)
return false;
return actor.hasPerm(user, "OWNER");
});
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`);
}
updates = updates.map(update => {
return {_id: update._id, flags: {dragRuler: update.dragRulerFlags}};
});
await combat.updateEmbeddedEntity("Combatant", updates, {diff: false});
}
export function recalculate(tokens) {
socket.executeForEveryone(_socketRecalculate, tokens ? tokens.map(token => token.id) : undefined);
}
function _socketRecalculate(tokenIds) {
return canvas.controls.ruler.dragRulerRecalculate(tokenIds);
}
+25 -1
View File
@@ -56,6 +56,22 @@ export class SpeedProvider {
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, ...)
* This function is only called if the TerrainLayer and TerrainRuler 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) {
// Lookup the cost for each square occupied by the token
const costs = area.map(space => terrainRuler.getCost(space.x, space.y, {token}));
// Return the maximum of the costs
return costs.reduce((max, current) => Math.max(max, current))
}
/**
* Returns a boolean indicating whether this token will use a Ruler or not.
* If this is returns `false` for a token Drag Ruler will be disabled for that token. Dragging a token for which this function
@@ -68,6 +84,14 @@ export class SpeedProvider {
return true
}
/**
* This hook is being called after Drag Ruler has updated the movement history for one or more tokens.
* It'll receive an array of tokens that have been updated.
* If your speed provider is storing any additional values that are relevant for the movement history, this function should
* await until those updates have completed inside foundry.
*/
async onMovementHistoryUpdate(tokens) {}
/**
* Returns the value that is currently set for the setting registered with the provided settingId.
*
@@ -108,7 +132,7 @@ export class GenericSpeedProvider extends SpeedProvider {
const speedAttribute = this.getSetting("speedAttribute")
if (!speedAttribute)
return []
const tokenSpeed = getProperty(token, speedAttribute)
const tokenSpeed = parseInt(getProperty(token, speedAttribute));
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).`)
return []
+10
View File
@@ -1,21 +1,31 @@
export function getDefaultSpeedAttribute() {
switch (game.system.id) {
case "dcc":
return "actor.data.data.attributes.speed.value";
case "dnd5e":
return "actor.data.data.attributes.movement.walk"
case "lancer":
return "actor.data.data.mech.speed"
case "pf1":
return "actor.data.data.attributes.speed.land.total"
case "shadowrun5e":
return "actor.data.data.movement.walk.value";
case "swade":
return "actor.data.data.stats.speed.value"
}
return ""
}
export function getDefaultDashMultiplier() {
switch (game.system.id) {
case "swade":
return 0
case "dcc":
case "dnd5e":
case "lancer":
case "pf1":
case "shadowrun5e":
return 2
}
return 0
+169
View File
@@ -0,0 +1,169 @@
import {getPixelsFromGridPosition} from "./foundry_fixes.js"
export function* zip(it1, it2) {
for (let i = 0;i < Math.min(it1.length, it2.length);i++) {
yield [it1[i], it2[i]]
}
}
export function getSnapPointForToken(x, y, token) {
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
return new PIXI.Point(x, y);
}
if (canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) {
if (token.getFlag("hex-size-support", "borderSize") % 2 === 0) {
const snapPoint = CONFIG.hexSizeSupport.findVertexSnapPoint(x, y, token, canvas.grid.grid)
return new PIXI.Point(snapPoint.x, snapPoint.y)
}
else {
return new PIXI.Point(...canvas.grid.getCenter(x, y))
}
}
// Tiny tokens can snap to the cells corners
if (!canvas.grid.isHex && token.data.width <= 0.5) {
const [topLeftX, topLeftY] = canvas.grid.getTopLeft(x, y);
const offsetX = x - topLeftX;
const offsetY = y - topLeftY;
const subGridWidth = Math.floor(canvas.grid.w / 2);
const subGridHeight = Math.floor(canvas.grid.h / 2);
const subGridPosX = Math.floor(offsetX / subGridWidth);
const subGridPosY = Math.floor(offsetY / subGridHeight);
return new PIXI.Point(topLeftX + (subGridPosX + 0.5) * subGridWidth, topLeftY + (subGridPosY + 0.5) * subGridHeight);
}
// Hex tokens, 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
if (canvas.grid.isHex || Math.round(token.data.width) % 2 === 1 || token.data.width < 1) {
return new PIXI.Point(...canvas.grid.getCenter(x, y))
}
// All remaining tokens (those with even or fractional multipliers on square grids) snap to the intersection points of the grid
const [snappedX, snappedY] = canvas.grid.getCenter(x - canvas.grid.w / 2, y - canvas.grid.h / 2)
return new PIXI.Point(snappedX + canvas.grid.w / 2, snappedY + canvas.grid.h / 2)
}
export function highlightTokenShape(position, shape, color, alpha) {
const layer = canvas.grid.highlightLayers[this.name];
if ( !layer )
return false;
const area = getAreaFromPositionAndShape(position, shape);
for (const space of area) {
const [x, y] = getPixelsFromGridPosition(space.x, space.y);
canvas.grid.grid.highlightGridPosition(layer, {x, y, color, alpha: 0.25 * alpha});
}
}
export function getAreaFromPositionAndShape(position, shape) {
return shape.map(space => {
let x = position.x + space.x;
let y = position.y + space.y;
if (canvas.grid.isHex) {
let shiftedRow;
if (canvas.grid.grid.options.even)
shiftedRow = 1
else
shiftedRow = 0
if (canvas.grid.grid.options.columns) {
if (space.x % 2 !== 0 && position.x % 2 !== shiftedRow) {
y += 1;
}
}
else {
if (space.y % 2 !== 0 && position.y % 2 !== shiftedRow) {
x += 1;
}
}
}
return {x, y}
});
}
export function getTokenShape(token) {
if (token.scene.data.gridType === CONST.GRID_TYPES.GRIDLESS) {
return [{x: 0, y: 0}]
}
else if (token.scene.data.gridType === CONST.GRID_TYPES.SQUARE) {
const topOffset = -Math.floor(token.data.height / 2)
const leftOffset = -Math.floor(token.data.width / 2)
const shape = []
for (let y = 0;y < token.data.height;y++) {
for (let x = 0;x < token.data.width;x++) {
shape.push({x: x + leftOffset, y: y + topOffset})
}
}
return shape
}
else {
// Hex grids
if (game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) {
const borderSize = token.data.flags["hex-size-support"].borderSize;
let shape = [{x: 0, y: 0}];
if (borderSize >= 2)
shape = shape.concat([{x: 0, y: -1}, {x: -1, y: -1}]);
if (borderSize >= 3)
shape = shape.concat([{x: 0, y: 1}, {x: -1, y: 1}, {x: -1, y: 0}, {x: 1, y: 0}]);
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}])
if (Boolean(CONFIG.hexSizeSupport.getAltOrientationFlag(token)) !== canvas.grid.grid.options.columns)
shape.forEach(space => space.y *= -1);
if (canvas.grid.grid.options.columns)
shape = shape.map(space => {return {x: space.y, y: space.x}});
return shape;
}
else {
return [{x: 0, y: 0}];
}
}
}
export function getTokenSize(token) {
let w, h;
const hexSizeSupportBorderSize = token.data.flags["hex-size-support"]?.borderSize;
if (hexSizeSupportBorderSize > 0) {
w = h = hexSizeSupportBorderSize
}
else {
w = token.data.width
h = token.data.height
}
return {w, h};
}
// Tokens that have a size divisible by two (2x2, 4x4, 2x1) have their ruler at the edge of a 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) {
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
return waypoints
}
const tokenSize = getTokenSize(token);
const waypointOffset = {x: 0, y: 0};
if (canvas.grid.isHex) {
if (game.modules.get("hex-size-support")?.active) {
const isAltOrientation = CONFIG.hexSizeSupport.getAltOrientationFlag(token);
if (canvas.grid.grid.options.columns) {
if (tokenSize.w % 2 === 0) {
waypointOffset.x = canvas.grid.w / 2;
if (!isAltOrientation)
waypointOffset.x *= -1;
}
}
else {
if (tokenSize.h % 2 === 0) {
waypointOffset.y = canvas.grid.h / 2;
if (isAltOrientation)
waypointOffset.y *= -1;
}
}
}
// If hex size support isn't active leave the waypoints like they are
}
else {
if (tokenSize.w % 2 === 0) {
waypointOffset.x = canvas.grid.w / 2;
}
if (tokenSize.h % 2 === 0) {
waypointOffset.y = canvas.grid.h / 2;
}
}
return waypoints.map(w => new PIXI.Point(w.x + waypointOffset.x, w.y + waypointOffset.y))
}