Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a5de9bbf0a | |||
| 15cda2cdbe | |||
| 709774b25f | |||
| bb8ac5d1ac | |||
| 5177746fbb |
@@ -1 +0,0 @@
|
|||||||
foundry-*.js
|
|
||||||
@@ -1,376 +0,0 @@
|
|||||||
## 1.11.2
|
|
||||||
### Bugfixes
|
|
||||||
- Fixed a memory leak that could cause the rule to slow down after using the pathfinding functionality for a while
|
|
||||||
|
|
||||||
### Misc
|
|
||||||
- GMs are now always allowed to use the pathfinding tool. The setting now only prevents players from using it.
|
|
||||||
|
|
||||||
### Compatibility
|
|
||||||
- Drag Ruler's generic speed provider is now aware of good defaults for Dungeonslayers 4
|
|
||||||
|
|
||||||
|
|
||||||
## 1.11.1
|
|
||||||
### Bugfixes
|
|
||||||
- Fixed a bug that would cause the pathfinding algorithm to make tokens of size 2 and 4 to take an unnecessary step
|
|
||||||
|
|
||||||
|
|
||||||
## 1.11.0
|
|
||||||
### New features
|
|
||||||
- Drag Ruler now supports pathfinding. Pressing the assigned button will automatically calculate the shortest route to the point you're dragging your token to and add the necessary waypoints to the ruler.
|
|
||||||
- This feature is only available for gridded maps
|
|
||||||
- This feature can only be used if it's enabled by the GM in the module settings
|
|
||||||
- The routing algorithm *does not* take difficult terrain into account
|
|
||||||
|
|
||||||
|
|
||||||
## 1.10.3
|
|
||||||
### Compatibility
|
|
||||||
- This release contains changes required to be compatible with Foundry 9.245
|
|
||||||
|
|
||||||
|
|
||||||
## 1.10.2
|
|
||||||
### Bugfixes
|
|
||||||
- Fixed a bug that could cause squares to be highlighted in the wrong color when using waypoints on a 5/10/5 gird
|
|
||||||
- When using Token Drag Vision, the temporary vision is now correctly cleaned up when dropping the token (resolves a conflict with the "Perfect Vision" module)
|
|
||||||
|
|
||||||
|
|
||||||
## 1.10.1
|
|
||||||
### Bugfixes
|
|
||||||
- Fixed a bug that caused keybindings to break if no scene is active
|
|
||||||
|
|
||||||
### Translation
|
|
||||||
- Updated Spanish translation (thanks to Viriato139ac#342)
|
|
||||||
|
|
||||||
|
|
||||||
## 1.10.0
|
|
||||||
**BREAKING** Drag Ruler 1.10.0 and onward cannot update directly from Drag Ruler versions older than 1.3.0. If you've been using Drag Ruler 1.2.2 or earlier in your world make sure to update to any Version between (inclusive) Drag Ruler 1.3.0 and 1.9.1, launch your world and log in as GM at least once. After doing so you can safely update to v1.10.0 or newer versions. Updating directly from 1.2.2 or older to 1.10.0 or newer will cause Drag Ruler to forget your Speed Prover Settings.
|
|
||||||
|
|
||||||
### New features
|
|
||||||
- Drag Ruler's key bindings can now be assigned to custom keys by the user
|
|
||||||
- Measuring difficult terrain on gridded maps with euclidean grid rule is now supported (for this to work the `Terrain Ruler` module needs to be enabled)
|
|
||||||
|
|
||||||
### Compatibility
|
|
||||||
- Drag Ruler now supports Foundry v9
|
|
||||||
- Drag Ruler now utilizes libwrapper to increase interoperability with other modules
|
|
||||||
|
|
||||||
### Translation
|
|
||||||
- Added french translation (thanks to Elfenduli)
|
|
||||||
- Updated japanese translation (thanks to touge)
|
|
||||||
|
|
||||||
|
|
||||||
## 1.9.1
|
|
||||||
### Bugfixes
|
|
||||||
- Fixed a bug that caused the ruler to misbehave or not show up at all if the speed provider isn't configured (this was a regression introduced in 1.9.0)
|
|
||||||
|
|
||||||
### Translation
|
|
||||||
- Updated the spaish translation (thanks to Viriato139ac#342)
|
|
||||||
|
|
||||||
|
|
||||||
## 1.9.0
|
|
||||||
### New features
|
|
||||||
- On Gridless scenes, tokens can now snap to their speed limits, to make full usage of a token's movement speed easier. This feature can be temporarily disabled by pressing Shift during drag and can be disabled completely in the settings.
|
|
||||||
|
|
||||||
### Bugfixes
|
|
||||||
- Non-square tokens (e.g. 2x1) now work correctly on square grids
|
|
||||||
- When modifying difficult terrain that a token has already moved over, this the movement history of the token won't change anymore (this was a regression introduced in 1.8.0)
|
|
||||||
- Fixed a bug that prevented pausing/unpausing the game when no scene was active
|
|
||||||
|
|
||||||
### API
|
|
||||||
- Added `dragRuler.getColorForDistanceAndToken` API endpoint that allows other modules to receive the highlight color for a specified distance with a given token.
|
|
||||||
|
|
||||||
|
|
||||||
## 1.8.2
|
|
||||||
### Compatibility
|
|
||||||
- The generic speed provider defaults have been updated for lance 1.0 (thanks to BoltsJ!)
|
|
||||||
- Eliminated a deprecation warning when both Drag Ruler and Hex Size Support are enabled (thanks Argonius-Angelus!)
|
|
||||||
|
|
||||||
|
|
||||||
## 1.8.1
|
|
||||||
### Bugfixes
|
|
||||||
- Fixed a bug where the function that was bound to the spacebar key wouldn't work correctly when the "Toggle Snap To Grid" module was enabled
|
|
||||||
|
|
||||||
### Translation
|
|
||||||
- Updated Spanish translation (thanks to Viriato139ac#342)
|
|
||||||
|
|
||||||
|
|
||||||
## 1.8.0
|
|
||||||
### New features
|
|
||||||
- Pressing escape during a drag now cancels the drag
|
|
||||||
- Undoing a movement via Ctrl+Z will now also remove that movement from Drag Ruler's movement history
|
|
||||||
- Drag Ruler can now configured to stay disabled by default when a Token/Template is being dragged. In that case it will activate once the button to place a waypoint is being pressed.
|
|
||||||
|
|
||||||
### Bugfixes
|
|
||||||
- Fixed a bug that caused the ruler to snap to grid when a waypoint was deleted while shift was being pressed (thanks to Michael Clavell!)
|
|
||||||
- Fixed a bug that could leave behind a single waypoint on the canvas when canceling a dragging operation while moving the mouse
|
|
||||||
|
|
||||||
### Compatibility
|
|
||||||
- Drag Ruler is now compatible with the "Toggle Snap To Grid" module (thanks to Michael Clavell!)
|
|
||||||
|
|
||||||
|
|
||||||
## 1.7.7
|
|
||||||
### Compatibility
|
|
||||||
- Updated the default settings for the swade game system. The new default speed attribute points to a speed value that gets adjusted for wounds.
|
|
||||||
|
|
||||||
|
|
||||||
## 1.7.6
|
|
||||||
### Translation
|
|
||||||
- Added Korean translation (thanks to KLO#1490)
|
|
||||||
- Added Spanish translation (thanks to Viriato139ac#342)
|
|
||||||
|
|
||||||
### Compatibility
|
|
||||||
- Drag Ruler's Generic Speed Provider is now awar of good default values for the "Call of Cthulhu 7th edition (Unofficial)" game system
|
|
||||||
- Drag Ruler is now compatible with Foundry 0.8.8
|
|
||||||
|
|
||||||
## 1.7.5
|
|
||||||
### Bugfixes
|
|
||||||
- Decimal speeds (as often used in metric game systems) are no longer being rounded down (thanks to DarKDinDoN for diagnosing this bug)
|
|
||||||
|
|
||||||
|
|
||||||
## 1.7.4
|
|
||||||
### Bugfixes
|
|
||||||
- Fixed a bug where the ruler would wrongly snap to the grid center for other players when dragging a measurement template
|
|
||||||
|
|
||||||
### Compatibility
|
|
||||||
- Drag Ruler is now compatiblie with the "Monk's Active Tile Triggers" module
|
|
||||||
- Drag Ruler's Generic Speed Provider is now aware of good default values for the D&D 3.5 game system
|
|
||||||
- Drag Ruler is now compatible with Foundry 0.8.7
|
|
||||||
|
|
||||||
## 1.7.3
|
|
||||||
### Compatibility
|
|
||||||
- Drag Ruler is now compatible with Foundry 0.8.5
|
|
||||||
|
|
||||||
|
|
||||||
## 1.7.2
|
|
||||||
### Bugfixes
|
|
||||||
- Fixed a bug that prevented waypoints for measurement templates from snapping to any other point than a grid cell corner (or grid cell center on hex)
|
|
||||||
- Fixed a bug that could cause the ruler to not end up at the token's center (especially if the token is being moved very quickly and then stopped abruptly)
|
|
||||||
|
|
||||||
|
|
||||||
## 1.7.1
|
|
||||||
### Bugfixes
|
|
||||||
- Fixed a bug that prevented players from moving their tokens ([#74](https://github.com/manuelVo/foundryvtt-drag-ruler/issues/74))
|
|
||||||
|
|
||||||
|
|
||||||
## 1.7.0
|
|
||||||
**BREAKING** This update is incompatible with previous Terrain Ruler versions. If you're using Terrain Ruler, make sure you update Terrain Ruler to at least version 1.3.0.
|
|
||||||
|
|
||||||
### New features
|
|
||||||
- A ruler will now be shown when dragging measurement templates over the map ([#13](https://github.com/manuelVo/foundryvtt-drag-ruler/issues/13))
|
|
||||||
- Drag Ruler can now measure difficult terrain on gridless maps (if the Terrain Ruler module is installed and enabled)
|
|
||||||
- Improved the positioning of the labels around the ruler. The labels should now never overlap with the waypoint.
|
|
||||||
|
|
||||||
### Bugfixes
|
|
||||||
- Fixed a bug that sometimes measured diagonals incorrectly with the 5/10/5 grid rule
|
|
||||||
- Fixed a bug that would cause the ruler to re-measure when the shift key is being pressed or released while a token is moving
|
|
||||||
|
|
||||||
### Compatibility
|
|
||||||
- Drag Ruler's Generic Speed Provider is now aware of good default values for the Starfinder game system
|
|
||||||
|
|
||||||
### Translation
|
|
||||||
- Corrected typos in the german translation (thanks to CarnVanBeck!)
|
|
||||||
|
|
||||||
### API
|
|
||||||
- The old API that Drag Ruler offered prior to version 1.3.0 is now deprecated. Speed Providers that still use this API will continue to work for now, but will generate a warning in the console about the deprecation. All modules and game systems offered on the FoundryVTT website have already updated to the new API. If you see the deprecation warning, please consider updating to the current version of the respective system/module you're using.
|
|
||||||
|
|
||||||
|
|
||||||
## 1.6.5
|
|
||||||
### Bugfixes
|
|
||||||
- Drag Ruler no longer gets 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
|
|
||||||
|
|
||||||
|
|
||||||
## 1.3.2
|
|
||||||
### Translation
|
|
||||||
- Updated japanese translation (thanks to touge)
|
|
||||||
|
|
||||||
|
|
||||||
## 1.3.1
|
|
||||||
### Bugfixes
|
|
||||||
- Fixed a bug where the coloring of ranges wouldn't work with the generic speed provider if the Dash Multiplier was set to 0
|
|
||||||
|
|
||||||
|
|
||||||
## 1.3.0
|
|
||||||
### New features
|
|
||||||
- The color used to indicate speed ranges is now configurable
|
|
||||||
- The settings dialog has been reworked
|
|
||||||
|
|
||||||
### System compatibility
|
|
||||||
- Drag Ruler's Generic SpeedProvider is now aware of good default values for the lancer game system (thanks to Grygon)
|
|
||||||
|
|
||||||
### API changes
|
|
||||||
This release introduces a new API that is incompatible with the old API. The new API offers more flexibility for users and Speed Providers alike and allows to add new features in the future without breaking compatibility again. The old API will continue to function, but to profit from any of the features below Speed Providers need to switch to the new API. For more details check out the API documentation.
|
|
||||||
|
|
||||||
The following things have changed with the new API:
|
|
||||||
- Colors used by speed providers can now be changed by the user via configuration
|
|
||||||
- Speed Providers can now offer settings to the user that will be integrated into Drag Ruler's settings menu
|
|
||||||
- Speed Providers can now conditionally disable Drag Ruler for some tokens
|
|
||||||
|
|
||||||
|
|
||||||
## 1.2.2
|
|
||||||
### Translation
|
|
||||||
- Added japanese translation (thanks to touge)
|
|
||||||
|
|
||||||
## 1.2.1
|
|
||||||
### Compatiblity
|
|
||||||
- Drag Ruler is now compatible with Hex Token Size Support. For compatibility Hex Token Size Support Version 0.5.4 or higher is required. Thanks to Ourobor for helping making this possible.
|
|
||||||
|
|
||||||
## 1.2.0
|
|
||||||
### New features
|
|
||||||
- Right click and spacebar can now be swapped, allowing to place waypoints with right click and removing them with spacebar
|
|
||||||
- The module can now be configured use a fixed color instead of the player color for the first speed range
|
|
||||||
- On gridless maps the ruler will now change it's color to indicate the different speed ranges
|
|
||||||
- As an alternative to right click (or spacebar, if you have swapped right and spacebar behavior) waypoints can now also be deleted with the `X` key
|
|
||||||
|
|
||||||
### Bugfixes
|
|
||||||
- Disabling snap to grid with shift now works as expected
|
|
||||||
- Fixed a bug where the ruler would sometimes jump to a different target location when deleting a waypoint
|
|
||||||
|
|
||||||
## 1.1.1
|
|
||||||
### Bugfixes
|
|
||||||
- Fixed a bug where tokens wouldn't be moved to the corect end position on gridless maps
|
|
||||||
- Ruler now appears immediately when the token is being dragged
|
|
||||||
- On gridless maps the ruler will always start measuring at the center of the token
|
|
||||||
- This change has no impact on the distance that is being measured
|
|
||||||
- In addition to the cosmetical aspect this also fixes a bug that allowed players to glitch through walls
|
|
||||||
|
|
||||||
## 1.1.0
|
|
||||||
### New features
|
|
||||||
- The drag ruler will now be colored for other players than the dragging player as well (only if they have at least observer permissions for that token)
|
|
||||||
- The drag ruler won't be shown to other players if they cannot see the dragged token
|
|
||||||
|
|
||||||
### Bugfixes
|
|
||||||
- Fixed a bug where Drag Ruler wouldn't work at all on some windows installations (specificially where the `foundry.js` has `CRLF` line endings)
|
|
||||||
|
|
||||||
## 1.0.1
|
|
||||||
### Bugfixes
|
|
||||||
- The GM can now move tokens through walls
|
|
||||||
- It is now possible to move multiple tokens with Drag Ruler enabled
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2021 Manuel Vögele
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
[](https://ko-fi.com/staebchenfisch)
|
|
||||||
|
|
||||||
# Drag Ruler
|
|
||||||
This module shows a ruler when you drag a token or measurement template to inform you how far you've dragged it from its start point. Additionally, if you're using a grid, the spaces the token will travel though will be colored depending on your tokens speed. By default, three colors are being used: green for spaces that your token can reach by walking normally are colored green, spaces that can only be reached by dashing will be colored yellow and spaces that cannot be reached with the token's speed will be colored red. If you're using a gridless map the ruler color will change to convey this information.
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
## Supports Tokens of all sizes
|
|
||||||
Terrain ruler has excellent support for tokens of all sizes. The Ruler will always originate from the token's center and will always highlight all the squares that tokens move over. If the [Hex Token Size Support](https://foundryvtt.com/packages/hex-size-support) is installed this is also true for large tokens on hex scenes.
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
## Difficult Terrain support
|
|
||||||
**To use support for difficult terrain you must install the [Terrain Ruler](https://foundryvtt.com/packages/terrain-ruler) module**
|
|
||||||
|
|
||||||
With the Terrain Ruler module installed, Drag Ruler is able to take difficult terrain that was placed down using the [Enhanced Terrain Layer](https://foundryvtt.com/packages/enhanced-terrain-layer) or [TerrainLayer](https://foundryvtt.com/packages/TerrainLayer) module into account.
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
## Movement history (optional)
|
|
||||||
*This feature can be disabled in the settings if you don't like it*
|
|
||||||
|
|
||||||
During combat, Drag Ruler will remember the path a token has taken during it's turn. When the token is being picked up again, Drag Ruler will continue measuring where it has left off. The path of the previous movement will be dipslayed in a faded color.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
The game systems that offer Drag Ruler integration are:
|
|
||||||
- Cypher System (starting with version 1.13.0)
|
|
||||||
- GURPS 4th Edition Game Aid (Unofficial) (starting with version 0.9.1)
|
|
||||||
- Ironclaw Second Edition (starting with version 0.2.2)
|
|
||||||
- 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))
|
|
||||||
- Pathfinder 1 (starting with version 0.77.3)
|
|
||||||
- Pathfinder 2e (via the module [PF2E Drag Ruler Integration](https://foundryvtt.com/packages/pf2e-dragruler/))
|
|
||||||
- Shadowrun 5th Edition (via the module [Drag Ruler Integration for Shadowrun 5E](https://foundryvtt.com/packages/drag-ruler-integration-for-shadowrun-5e))
|
|
||||||
- Starfinder (via the module [Starfinder Drag Ruler Integration](https://foundryvtt.com/packages/starfinder-drag-ruler))
|
|
||||||
- Stargate RPG (starting with version 1.6.0)
|
|
||||||
- Symbaroum (via the module [Symbaroum drag ruler integration](https://foundryvtt.com/packages/symbaroum-drag-ruler-integration))
|
|
||||||
- Tagmar RPG (starting with version 1.1.4)
|
|
||||||
- TheWitcherTRPG (starting with version 0.0.62)
|
|
||||||
- Tormenta20 (starting with version 1.1.37)
|
|
||||||
- Shadow of the Demon Lord (starting with version 1.7.15)
|
|
||||||
- Wasteland Ventures (starting with version 0.1.0)
|
|
||||||
- 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)
|
|
||||||
- Korean (thanks to KLO#1490)
|
|
||||||
- Spanish (thanks to Viriato139ac#342)
|
|
||||||
|
|
||||||
## API
|
|
||||||
*Audience: This paragraph is intended for module and system devleopers that want to add more complex behavior to Drag Ruler. If you just want to use this plugins features skip this paragraph.*
|
|
||||||
|
|
||||||
The path coloring behavior of Drag Ruler can be altered by modules and systems to allow for for more complex coloring than provided by default. This allows specifying custom colors, using more different colors than offered by default and performing more calculations for determining the colors (for example a token may only be allowd to run if it isn't waring armor). Doing so is simple. This paragraph will provide an example by showing an implementation of the api for a fictional game system, that contains everything you need to get started. Afterwards the code will be dissected into small parts and explained.
|
|
||||||
|
|
||||||
### Full example
|
|
||||||
```javascript
|
|
||||||
Hooks.once("dragRuler.ready", (SpeedProvider) => {
|
|
||||||
class FictionalGameSystemSpeedProvider extends SpeedProvider {
|
|
||||||
get colors() {
|
|
||||||
return [
|
|
||||||
{id: "walk", default: 0x00FF00, name: "my-module-id.speeds.walk"},
|
|
||||||
{id: "dash", default: 0xFFFF00, name: "my-module-id.speeds.dash"},
|
|
||||||
{id: "run", default: 0xFF8000, name: "my-module-id.speeds.run"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
getRanges(token) {
|
|
||||||
const baseSpeed = token.actor.data.speed
|
|
||||||
|
|
||||||
// A character can always walk it's base speed and dash twice it's base speed
|
|
||||||
const ranges = [
|
|
||||||
{range: baseSpeed, color: "walk"},
|
|
||||||
{range: baseSpeed * 2, color: "dash"}
|
|
||||||
]
|
|
||||||
|
|
||||||
// Characters that aren't wearing armor are allowed to run with three times their speed
|
|
||||||
if (!token.actor.data.isWearingArmor) {
|
|
||||||
ranges.push({range: baseSpeed * 3, color: "dash"})
|
|
||||||
}
|
|
||||||
|
|
||||||
return ranges
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dragRuler.registerModule("my-module-id", FictionalGameSystemSpeedProvider)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Exmplanation of the code
|
|
||||||
```javascript
|
|
||||||
Hooks.once("dragRuler.ready", (SpeedProvider) => {
|
|
||||||
class FictionalGameSystemSpeedProvider extends SpeedProvider {
|
|
||||||
```
|
|
||||||
|
|
||||||
After Drag Ruler has initialized and is ready to receive API calls it will fire the `dragRuler.ready` event. This is the signal for your module/gamesystem that it can now register itself in Drag Ruler. To do this you'll need to implement a Speed Provider. The Hook will provide you with one Argument: The class `SpeedProvider`, which serves as base class for all speed providers. To implement a Speed Provider you create a subclass of `SpeedProvider`. Within that class you override functions of the base class to implement the functionality you need. The functions `colors` and `getRanges` must be overridden by all Speed Provider implementations. Overriding other functions of the Speed Provider is optional and can be done if you need additional functionality for your speed provider.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
get colors() {
|
|
||||||
return [
|
|
||||||
{id: "walk", default: 0x00FF00, name: "my-module-id.speeds.walk"},
|
|
||||||
{id: "dash", default: 0xFFFF00, name: "my-module-id.speeds.dash"},
|
|
||||||
{id: "run", default: 0xFF8000, name: "my-module-id.speeds.run"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The getter `colors` is one of the two functions that must be overridden by all implementations of `SpeedProvider`. It must return an array of all colors, that may be used by this speed provider. Each color must be an object and has three attributes:
|
|
||||||
|
|
||||||
- *id*: A name for this color that identifies this color. It will be used in other functions within your speed provider to reference to this color. Ther must not be two colors with the same id.
|
|
||||||
- *default*: The default color value that should be used by this color.
|
|
||||||
- *name*: A human readable name for this color that will be used in the Speed Provider Settings dialog. Drag Ruler will try to internationalize this string. This field is optional, but it's highly recommended to use it.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
getRanges(token) {
|
|
||||||
const baseSpeed = token.actor.data.speed
|
|
||||||
|
|
||||||
// A character can always walk it's base speed and dash twice it's base speed
|
|
||||||
const ranges = [
|
|
||||||
{range: baseSpeed, color: "walk"},
|
|
||||||
{range: baseSpeed * 2, color: "dash"}
|
|
||||||
]
|
|
||||||
|
|
||||||
// Characters that aren't wearing armor are allowed to run with three times their speed
|
|
||||||
if (!token.actor.data.isWearingArmor) {
|
|
||||||
ranges.push({range: baseSpeed * 3, color: "dash"})
|
|
||||||
}
|
|
||||||
|
|
||||||
return ranges
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The `getRanges` function is the second function that every Speed Provider must override. This function receives a token as a parameter and must return an array of all the ranges that this token can reach. Each range is represented by an object having these fields:
|
|
||||||
- *range*: The maximum distance a token is allowed to move within this range
|
|
||||||
- *color*: The id of the color that is used to represent this range. This id must match the id as defined in the `colors` getter.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
dragRuler.registerModule("my-module-id", FictionalGameSystemSpeedProvider)
|
|
||||||
```
|
|
||||||
|
|
||||||
This line registers the Speed Provider class that was just created with Drag Ruler. The paramter must be the id of the module you're writing. This id must exactly match the id specified in you manifest. As the second parameter the Speed Provider class that was just created is passed in.
|
|
||||||
|
|
||||||
If you're not writing a module but a game system use `dragRuler.registerSystem` instead of `dragRuler.registerModule.
|
|
||||||
|
|
||||||
### Additional capabilities of the API
|
|
||||||
In addition to the basic capabilities of the API presented in the example above, Drag Ruler's API offers more capabilities, like adding settings to your Speed Provider. To learn more about additional capabilities refer to the documentation of the `SpeedProvider` base class in [speed_provider.js](src/speed_provider.js).
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
{
|
|
||||||
"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 schwieriges 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"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"keybindings": {
|
|
||||||
"cancelDrag": "Ziehen beenden",
|
|
||||||
"createWaypoint": "Wegpunkt erstellen",
|
|
||||||
"deleteWaypoint": "Wegpunkt löschen",
|
|
||||||
"disableSnap": {
|
|
||||||
"name": "Raster deaktivieren",
|
|
||||||
"hint": "Während diese Taste gedrückt ist wird das Raster temporär deaktiviert"
|
|
||||||
},
|
|
||||||
"moveWithoutAnimation": {
|
|
||||||
"name": "Token animation deaktivieren",
|
|
||||||
"hint": "Wenn diese Taste gedrückt wird, während ein Token fallen gelassen wird, bewegt es sich ohne Animation zum Zielort."
|
|
||||||
},
|
|
||||||
"togglePathfinding": {
|
|
||||||
"name": "Wegfindung umschalten",
|
|
||||||
"hint": "Wenn diese Taste gedrückt gehalten wird, während ein Token gezogen wird, wird die Wegfindung vorübergehend aktiviert/deaktiviert"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"allowPathfinding": {
|
|
||||||
"name": "Wegfindung für Spieler erlauben",
|
|
||||||
"hint": "Erlaubt es Spielern die Wegfindungs zu benutzen. Bitte beachte, dass die Wegfindung Wege durch unerkundeten Nebel des Kriegs und Ätherische Wände berechnen kann. Dies kann deinen Spielern Geheimnisse lüften, von denen sie noch nicht erfahren sollten."
|
|
||||||
},
|
|
||||||
"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."
|
|
||||||
},
|
|
||||||
"autoStartMeasurement": {
|
|
||||||
"name": "Automatisch mit Messen beginnen",
|
|
||||||
"hint": "Wenn diese Option aktiviert ist beginnt Drag Ruler mit dem Messen sobald das Token bewegt wird. Wenn die Option deaktiviert ist bleibt Drag Ruler deaktiviert bis der Knopf zum platzieren eines Wegpunkts betätigt wird."
|
|
||||||
},
|
|
||||||
"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."
|
|
||||||
},
|
|
||||||
"rightClickAction": {
|
|
||||||
"name": "Aktion bei Rechtsklick",
|
|
||||||
"hint": "Was soll getan werden, wenn die rechte Maustaste während des Ziehens gedrückt wird?",
|
|
||||||
"choices": {
|
|
||||||
"create": "Wegpunkt erstellen",
|
|
||||||
"delete": "Wegpunkt löschen",
|
|
||||||
"cancel": "Ziehen beenden"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"showGMRulerToPlayers": {
|
|
||||||
"name": "Lineal des Spielleiters 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 stehenden 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ängig, welche Spielsystemintegration hier ausgewählt wurde.",
|
|
||||||
"choices": {
|
|
||||||
"module": "Modul {name}",
|
|
||||||
"native": "Generisch",
|
|
||||||
"system": "System {name}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"useGridlessraster": {
|
|
||||||
"name": "Geschwindigkeitsraster aktivieren",
|
|
||||||
"hint": "Lässt Spielfiguren auf gitterlosen Szenen an deren Geschwindigkeitsgrenzen einrasten."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,5 @@
|
|||||||
{
|
{
|
||||||
"drag-ruler": {
|
"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": {
|
"settings": {
|
||||||
"dashMultiplier": {
|
"dashMultiplier": {
|
||||||
"name": "Dash Multiplier",
|
"name": "Dash Multiplier",
|
||||||
@@ -22,100 +8,16 @@
|
|||||||
"speedAttribute": {
|
"speedAttribute": {
|
||||||
"name": "Speed Attribute",
|
"name": "Speed Attribute",
|
||||||
"hint": "The attribute that defines a token's walking speed. This is used during coloring of the measured path."
|
"hint": "The attribute that defines a token's walking speed. This is used during coloring of the measured path."
|
||||||
}
|
|
||||||
},
|
|
||||||
"speeds": {
|
|
||||||
"walk": "walk",
|
|
||||||
"dash": "dash"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"keybindings": {
|
|
||||||
"cancelDrag": "Cancel dragging",
|
|
||||||
"createWaypoint": "Create waypoint",
|
|
||||||
"deleteWaypoint": "Delete waypoint",
|
|
||||||
"disableSnap": {
|
|
||||||
"name": "Disable snapping",
|
|
||||||
"hint": "Snapping will be temporarily disabled while this key is being held down"
|
|
||||||
},
|
|
||||||
"moveWithoutAnimation": {
|
|
||||||
"name": "Disable token animation",
|
|
||||||
"hint": "When being held while dropping a token, the token will move to the target location without animating"
|
|
||||||
},
|
|
||||||
"togglePathfinding": {
|
|
||||||
"name": "Toggle pathfinding",
|
|
||||||
"hint": "When being held while dragging a token, the pathfinding functionality will be temporarily enabled/disabled"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"allowPathfinding": {
|
|
||||||
"name": "Allow pathfinding for players",
|
|
||||||
"hint": "Allows players to use Drag Ruler's pathfinding functionality in this world. Be aware that pathfinding can route through unexplored fog of war and Ethereal Walls, which might reveal secrets to your players ahead of time."
|
|
||||||
},
|
|
||||||
"alwaysShowSpeedForPCs": {
|
|
||||||
"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."
|
|
||||||
},
|
|
||||||
"autoStartMeasurement": {
|
|
||||||
"name": "Automatically start measuring",
|
|
||||||
"hint": "If enabled, Drag Ruler will start measuring as soon as the token is being dragged. If disabled, Drag Ruler will remain inactive and will only start measuring once the button to add a waypoint is being pressed."
|
|
||||||
},
|
|
||||||
"autoPathfinding": {
|
|
||||||
"name": "Pathfinding by default",
|
|
||||||
"hint": "If enabled, dragging a token will automatically use a pathfinding ruler"
|
|
||||||
},
|
|
||||||
"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."
|
|
||||||
},
|
|
||||||
"rightClickAction": {
|
|
||||||
"name": "Right click action",
|
|
||||||
"hint": "What action shall be performed when right clicking while dragging a token?",
|
|
||||||
"choices": {
|
|
||||||
"create": "Create waypoint",
|
|
||||||
"delete": "Delete waypoint",
|
|
||||||
"cancel": "Cancel drag"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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.",
|
|
||||||
"button": "Speed Provider Settings",
|
|
||||||
"windowTitle": "Speed Provider Settings",
|
|
||||||
"headers": {
|
|
||||||
"speedProvider": "Speed Provider",
|
|
||||||
"speedProviderSettings": "Speed Provider specific settings"
|
|
||||||
},
|
|
||||||
"activeProvider": {
|
|
||||||
"name": "Currently active Speed Provider",
|
|
||||||
"hint": "The settings show below depend on the active speed provider. If the GM selects a different speed provider the available settings may change."
|
|
||||||
},
|
|
||||||
"noSettings": "This speed provider doesn't offer any configuration options.",
|
|
||||||
"color": {
|
|
||||||
"name": "Color for {colorName}",
|
|
||||||
"hint": "The color that will be used to color squares that are within {colorName} range",
|
|
||||||
"unreachable": {
|
|
||||||
"name": "unreachable",
|
|
||||||
"hint": "The color for spaces that aren't reachable by the dragged token"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"speedProvider": {
|
"speedProvider": {
|
||||||
"name": "Speed Settings Provider",
|
"name": "Speed Settings Provider",
|
||||||
"hint": "Select who provides speed information for tokens duing coloring. Drag Ruler offers a generic speed provider that provides basic functionality and should work for all game systems if configured correctly. More speed providers can be made available via game systems and installed modules. Selecting a different speed provider than the generic speed provider may offer a better integration into the rules of the game system you're using. The options below are dependent upon the speed provider selected here.",
|
"hint": "Select who provides speed information for tokens duing coloring. Using a game system or module may provide more flexible coloring than sticking to the options provided by Drag Ruler.",
|
||||||
"choices": {
|
"choices": {
|
||||||
"module": "Module {name}",
|
"module": "Module",
|
||||||
"native": "Generic",
|
"native": "Drag Ruler",
|
||||||
"system": "System {name}"
|
"system": "System"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"useGridlessRaster": {
|
|
||||||
"name": "Use speed based snapping",
|
|
||||||
"hint": "On Gridless scenes, this makes tokens snap to the token's speed ranges."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
{
|
|
||||||
"drag-ruler": {
|
|
||||||
"I18N": {
|
|
||||||
"LANGUAGE": "Español",
|
|
||||||
"MAINTAINERS": "@Viriato139ac#0342"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"ok": "OK",
|
|
||||||
"terrain-ruler": {
|
|
||||||
"title": "Cómo medir a través de terreno difícil con 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": "No volver a mostrar"
|
|
||||||
},
|
|
||||||
"socketlib": {
|
|
||||||
"title": "El módulo Socketlib no está instalado",
|
|
||||||
"text": "Drag Ruler necesita para su funcionamiento correcto que instale socketlib. Por favor, active socketlib en este mundo"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"resetMovementHistory": "Reiniciar historial de movimiento",
|
|
||||||
"genericSpeedProvider": {
|
|
||||||
"settings": {
|
|
||||||
"dashMultiplier": {
|
|
||||||
"name": "Multiplicador por correr",
|
|
||||||
"hint": "Se puede usar para aplicar a los iconos una velocidad secundaria que use un color distinto en la medición de la ruta a seguir. Establece el valor a 0 para deshabilitar esta velocidad secundaria"
|
|
||||||
},
|
|
||||||
"speedAttribute": {
|
|
||||||
"name": "Attributo de velocidad",
|
|
||||||
"hint": "El atributo que define la velocidad de movimiento andando del icono. Se usa para los códigos de colores al medir la ruta a seguir"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"speeds": {
|
|
||||||
"walk": "caminar",
|
|
||||||
"dash": "correr"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"keybindings": {
|
|
||||||
"cancelDrag": "Cancelar arrastre",
|
|
||||||
"createWaypoint": "Crear punto de ruta",
|
|
||||||
"deleteWaypoint": "Borrar punto de ruta",
|
|
||||||
"disableSnap": {
|
|
||||||
"name": "Deshabilitar ajuste",
|
|
||||||
"hint": "El ajuste a la rejilla se deshabilitará temporalmente mientras se presione esta tecla"
|
|
||||||
},
|
|
||||||
"moveWithoutAnimation": {
|
|
||||||
"name": "Deshabilitar animación de icono",
|
|
||||||
"hint": "Si al soltar un icono se presiona esta tecla, se deshabilitará la animación del icono al moverse al destino"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"alwaysShowSpeedForPCs": {
|
|
||||||
"name": "Mostrar velocidad de los PJs a todo el mundo",
|
|
||||||
"hint": "Si se habilita, se mostrará a todo el mundo los códigos de colores de las rutas de los PJs, incluso si no tienen permisos de observador para ese personaje"
|
|
||||||
},
|
|
||||||
"autoStartMeasurement": {
|
|
||||||
"name": "Comenzar a medir automáticamente",
|
|
||||||
"hint": "Si se habilita, Drag Ruler comenzará a medir en cuanto se comience a arrastrar un icono. Si se deshabilita, Drag Ruler permanecerá inactivo y comenzará a medir únicamente cuando se presione el botón configurado para añadir un nuevo punto de ruta"
|
|
||||||
},
|
|
||||||
"enableMovementHistory": {
|
|
||||||
"name": "Habilitar historial de movimiento durante el combate",
|
|
||||||
"hint": "Si se habilita, Drag Ruler recordará la ruta que ha seguido un icono en su turno y la mostrará al seleccionarlo de nuevo"
|
|
||||||
},
|
|
||||||
"rightClickAction": {
|
|
||||||
"name": "Acción del botón derecho",
|
|
||||||
"hint": "Qué acción se llevará a cabo cuando se haga clic derecho mientras se arrastra un icono",
|
|
||||||
"choices": {
|
|
||||||
"create": "Crear punto de ruta",
|
|
||||||
"delete": "Borrar punto de ruta",
|
|
||||||
"cancel": "Cancelar arrastre"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"showGMRulerToPlayers": {
|
|
||||||
"name": "Mostrar regla del GM a los jugadores",
|
|
||||||
"hint": "Si se deshabilita, no se mostrará la regla de medición de los GMs al resto de usuarios"
|
|
||||||
},
|
|
||||||
"speedProviderSettings": {
|
|
||||||
"name": "Proveedor de configuración de velocidad",
|
|
||||||
"hint": "El proveedor de ajustes de velocidad contiene todos los ajustes específicos de cada sistema",
|
|
||||||
"button": "Proveedor de configuración de velocidad",
|
|
||||||
"windowTitle": "Proveedor de configuración de velocidad",
|
|
||||||
"headers": {
|
|
||||||
"speedProvider": "Proveedor de velocidad",
|
|
||||||
"speedProviderSettings": "Configuraciones específicas del proveedor de velocidad"
|
|
||||||
},
|
|
||||||
"activeProvider": {
|
|
||||||
"name": "Proveedor de velocidad actualmente activo",
|
|
||||||
"hint": "La configuración que se muestra más abajo depende del proveedor de velocidad activo. Si el GM selecciona un proveedor distinto, los ajustes disponibles puede que cambien"
|
|
||||||
},
|
|
||||||
"noSettings": "Este proveedor de velocidad no dispone de ninguna opción de configuración",
|
|
||||||
"color": {
|
|
||||||
"name": "Color para {colorName}",
|
|
||||||
"hint": "Color que se usará para establecer el tono de las cuadrículas que se encuentran dentro del rango {colorName}",
|
|
||||||
"unreachable": {
|
|
||||||
"name": "inalcanzable",
|
|
||||||
"hint": "Color que se usará para las cuadrículas más allá de la distancia máxima de movimiento del icono"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"speedProvider": {
|
|
||||||
"name": "Proveedor de configuración de velocidad",
|
|
||||||
"hint": "Seleccione quien suministra la información de la velocidad de los iconos a la hora de colorear la ruta a seguir. Drag Ruler incluye un proveedor genérico que ofrece una funcionalidad básica y que probablemente funcione para casi todos los sistemas si se usa de forma correcta. Se pueden añadir otros proveedores de velocidad al instalar sistemas o módulos. Seleccionar un proveedor distinto al genérico asegurará una mejor integración con las reglas del sistema que está usando. Las opciones siguientes dependerán del proveedor que seleccione",
|
|
||||||
"choices": {
|
|
||||||
"module": "Módulo {name}",
|
|
||||||
"native": "Genérico",
|
|
||||||
"system": "Sistema {name}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"useGridlessRaster": {
|
|
||||||
"name": "Usar ajuste a la velocidad",
|
|
||||||
"hint": "Cuando no se use rejilla en la escena, este ajuste hace que los iconos se ajusten a sus rangos de velocidad. Se puede deshabilitar temporalmente pulsando la tecla Mayús mientras se arrastra"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
{
|
|
||||||
"drag-ruler": {
|
|
||||||
"dependencies": {
|
|
||||||
"ok": "OK",
|
|
||||||
"terrain-ruler": {
|
|
||||||
"title": "Comment mesurer un terrain difficile avec Drag Ruler",
|
|
||||||
"text": "Vous avez le module {moduleName} activé. Drag Ruler est capable de mesurer un terrain difficile qui a été placé à l'aide de ce module. Pour rendre cela possible, Drag Ruler utilise le module Terrain Ruler. Si Terrain Ruler est installé et activé Drag Ruler commencera automatiquement à respecter le terrain difficile dans ses mesures.",
|
|
||||||
"neverShowAgain": "Ne plus jamais afficher cela"
|
|
||||||
},
|
|
||||||
"socketlib": {
|
|
||||||
"title": "Module Socketlib manquant",
|
|
||||||
"text": "Drag Ruler nécessite que le module socketlib fonctionne correctement. Veuillez activer le module socketlib dans ce monde."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"resetMovementHistory": "Réinitialiser l'historique des mouvements",
|
|
||||||
"genericSpeedProvider": {
|
|
||||||
"settings": {
|
|
||||||
"dashMultiplier": {
|
|
||||||
"name": "Multiplicateur de courses",
|
|
||||||
"hint": "Cela peut être utilisé pour donner aux jetons une vitesse secondaire pendant la coloration du chemin mesuré. Réglez-le sur 0 pour désactiver la vitesse secondaire."
|
|
||||||
},
|
|
||||||
"speedAttribute": {
|
|
||||||
"name": "Attribut de vitesse",
|
|
||||||
"hint": "L'attribut qui définit la vitesse de marche d'un jeton. Il est utilisé lors de la coloration du chemin mesuré." }
|
|
||||||
},
|
|
||||||
"speeds": {
|
|
||||||
"walk": "marche",
|
|
||||||
"dash": "course"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"alwaysShowSpeedForPCs": {
|
|
||||||
"name": "Afficher la vitesse du PJ à tout le monde",
|
|
||||||
"hint": "Si activé, la coloration basée sur la vitesse de l'acteur pour les personnages joueurs sera montrée à tout le monde, même s'ils n'ont pas la permission d'observateur pour la feuille de personnage."
|
|
||||||
},
|
|
||||||
"autoStartMeasurement": {
|
|
||||||
"name": "Démarrer automatiquement la mesure",
|
|
||||||
"hint": "Si activé, Drag Ruler commencera à mesurer dès que le jeton est glissé. S'il est désactivé, Drag Ruler restera inactif et ne commencera à mesurer qu'une fois que le bouton pour ajouter un waypoint est enfoncé."
|
|
||||||
},
|
|
||||||
"enableMovementHistory": {
|
|
||||||
"name": "Activer l'historique des mouvements pendant le combat",
|
|
||||||
"hint": "Si activé, Drag Ruler se souviendra du chemin emprunté par un jeton pendant son tour de combat et l'affichera lorsque vous récupérerez le jeton."
|
|
||||||
},
|
|
||||||
"showGMRulerToPlayers": {
|
|
||||||
"name": "Afficher la règle MJ aux joueurs",
|
|
||||||
"hint": "Si désactivé, la règle des MJ ne sera pas affichée pour les joueurs non-MJ"
|
|
||||||
},
|
|
||||||
"speedProviderSettings": {
|
|
||||||
"name": "Paramètres du système de vitesse",
|
|
||||||
"hint": "Les paramètres du système de vitesse contiennent tous les paramètres spécifiques au système de jeu.",
|
|
||||||
"button": "Paramètres du système de vitesse",
|
|
||||||
"windowTitle": "Paramètres du système de vitesse",
|
|
||||||
"headers": {
|
|
||||||
"speedProvider": "système de vitesse",
|
|
||||||
"speedProviderSettings": "Paramètres spécifiques au système de vitesse"
|
|
||||||
},
|
|
||||||
"activeProvider": {
|
|
||||||
"name": "système de vitesse actuellement actif",
|
|
||||||
"hint": "Les paramètres indiqués ci-dessous dépendent du système de vitesse actif. Si le MJ sélectionne un système de vitesse différent, les paramètres disponibles peuvent changer."
|
|
||||||
},
|
|
||||||
"noSettings": "Ce système de vitesse n'offre aucune option de configuration.",
|
|
||||||
"color": {
|
|
||||||
"name": "Couleur pour {colorName}",
|
|
||||||
"hint": "La couleur qui sera utilisée pour colorier les carrés qui sont dans la plage {colorName}",
|
|
||||||
"unreachable": {
|
|
||||||
"name": "injoignable",
|
|
||||||
"hint": "La couleur des espaces qui ne sont pas accessibles par le jeton déplacé"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"speedProvider": {
|
|
||||||
"name": "système de paramètres de vitesse",
|
|
||||||
"indice": "Sélectionnez qui fournit les informations de vitesse pour les jetons lors de la coloration. Drag Ruler offre un système de vitesse générique qui fournit des fonctionnalités de base et devrait fonctionner pour tous les systèmes de jeu s'il est correctement configuré. Plus de systèmes de vitesse peuvent être mis à disposition via les systèmes de jeu et les modules installés . La sélection d'un autre système de vitesse que le système de vitesse générique peut offrir une meilleure intégration dans les règles du système de jeu que vous utilisez. Les options ci-dessous dépendent du système de vitesse sélectionné ici.",
|
|
||||||
"choices": {
|
|
||||||
"module": "Module {name}",
|
|
||||||
"native": "Générique",
|
|
||||||
"system": "Systéme {name}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"useGridlessRaster": {
|
|
||||||
"name": "Use speed based snapping",
|
|
||||||
"hint": "On Gridless scenes, this makes tokens snap to the token's speed ranges. This can be temporarily disabled by pressing Shift during drag."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
{
|
|
||||||
"Localization Provided By": "touge",
|
|
||||||
"drag-ruler": {
|
|
||||||
"dependencies": {
|
|
||||||
"ok": "OK",
|
|
||||||
"terrain-ruler": {
|
|
||||||
"title": "Drag Rulerで移動困難地形を測定する",
|
|
||||||
"text": "「{moduleName}」モジュールが有効化されました。Drag Rulerは、同モジュールによって設定された移動困難地形を測定できます。これには「Terrain Ruler」モジュールが必要です。有効化すると、自動的に移動困難地形を考慮した測定を行います",
|
|
||||||
"neverShowAgain": "再表示しない"
|
|
||||||
},
|
|
||||||
"socketlib": {
|
|
||||||
"title": "「Socketlib」が見つかりません",
|
|
||||||
"text": "Drag Rulerの正常動作には「Socketlib」モジュールが必要です。このワールドで「Socketlib」を有効にしてください。"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"resetMovementHistory": "移動履歴のリセット",
|
|
||||||
"genericSpeedProvider": {
|
|
||||||
"settings": {
|
|
||||||
"dashMultiplier": {
|
|
||||||
"name": "早足の倍率",
|
|
||||||
"hint": "設定すると、移動経路の測定中に第二の色分けが追加されます。0にすると第二速度が無効になります。"
|
|
||||||
},
|
|
||||||
"speedAttribute": {
|
|
||||||
"name": "移動速度の属性値",
|
|
||||||
"hint": "コマの移動速度が定義された属性値を指定します。移動範囲表示の色分けに使用されます。"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"speeds": {
|
|
||||||
"walk": "歩き",
|
|
||||||
"dash": "早足"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"alwaysShowSpeedForPCs": {
|
|
||||||
"name": "プレイヤーキャラクターの移動速度を全員に公開する",
|
|
||||||
"hint": "有効にすると、アクターの観察者(Observer)権限を持たない人にも,移動範囲の色分けが表示されるようになります。"
|
|
||||||
},
|
|
||||||
"autoStartMeasurement": {
|
|
||||||
"name": "自動計測",
|
|
||||||
"hint": "有効化時、Drag Rulerはトークンがドラッグされた時点で計測を初めます。無効化の場合、ドラッグしてもDrag Rulerは起動せず、中間地点をおいたときにのみ起動します。"
|
|
||||||
},
|
|
||||||
"enableMovementHistory": {
|
|
||||||
"name": "戦闘中の移動履歴を有効にする",
|
|
||||||
"hint": "有効にすると、Drag Rulerは戦闘ターン中にコマが辿った経路を記憶するようになります。これはコマの再選択時に再表示されます。"
|
|
||||||
},
|
|
||||||
"showGMRulerToPlayers": {
|
|
||||||
"name": "GMのルーラーをプレイヤーに見せる",
|
|
||||||
"hint": "無効の場合、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": "色分けに使用する速度情報の提供元を選択します。Drag Rulerは基本機能を備えた汎用の提供元を備えており、正しく設定すればすべてのゲームシステムで動作するはずです。しかしゲームシステムやモジュールを介することで、より多くの提供元を利用することもできます。こうした提供元を利用すると、ゲームルールとの統合がさらに向上するかもしれません。以下のオプションは、ここで選択した提供元によって変化します。",
|
|
||||||
"choices": {
|
|
||||||
"module": "モジュール {name}",
|
|
||||||
"native": "汎用",
|
|
||||||
"system": "システム {name}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"useGridlessRaster": {
|
|
||||||
"name": "移動速度制限",
|
|
||||||
"hint": "グリッドの無いシーンでは、コマがドラッグできる範囲を移動速度ぶんまで制限します。Shiftキーを押すことで一時的にこの制限を無視できます。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
{
|
|
||||||
"drag-ruler": {
|
|
||||||
"dependencies": {
|
|
||||||
"ok": "OK",
|
|
||||||
"terrain-ruler": {
|
|
||||||
"title": "Drag Ruler를 사용한 험지 측정 방법",
|
|
||||||
"text": "{moduleName} 모듈을 활성화했습니다. Drag Ruler는 해당 모듈을 사용하여 험지를 측정할 수 있습니다. 이렇게 하려면 Terrain Ruler 모듈을 활성화하십시오. Terrain Ruler 모듈을 설치하고 Drag Ruler를 활성화하면 자동적으로 험지를 측정합니다.",
|
|
||||||
"neverShowAgain": "다시 표시하지 않음"
|
|
||||||
},
|
|
||||||
"socketlib": {
|
|
||||||
"title": "Socketlib 모듈 비탐지",
|
|
||||||
"text": "Drag Ruler는 Socketlib 모듈이 있어야 합니다. 이 월드에서 해당 모듈을 활성화하십시오."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"resetMovementHistory": "이동 내역 리셋",
|
|
||||||
"genericSpeedProvider": {
|
|
||||||
"settings": {
|
|
||||||
"dashMultiplier": {
|
|
||||||
"name": "질주 승수",
|
|
||||||
"hint": "측정된 경로에 색을 입힐 때 추가 속도를 제공하는 데 사용할 수 있다. 추가 속도를 비활성화하려면 이 값을 0으로 설정한다."
|
|
||||||
},
|
|
||||||
"speedAttribute": {
|
|
||||||
"name": "속도 어트리뷰트",
|
|
||||||
"hint": "토큰의 이동속도를 정의하는 속성이다. 측정 경로에 색을 입힐 때 사용한다."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"speeds": {
|
|
||||||
"walk": "보행",
|
|
||||||
"dash": "질주"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"alwaysShowSpeedForPCs": {
|
|
||||||
"name": "PC 속도를 모두에게 표시",
|
|
||||||
"hint": "활성화시 플레이어 캐릭터에 대한 관찰자 권한이 없더라도 액터 속도에 따라 색상이 모든 유저에게 표시된다."
|
|
||||||
},
|
|
||||||
"enableMovementHistory": {
|
|
||||||
"name": "전투 중 이동 기록 사용",
|
|
||||||
"hint": "활성화시 Drag Ruler는 해당 턴에 토큰이 이동한 경로를 기억하고 다시 이동시킬 때 표시한다."
|
|
||||||
},
|
|
||||||
"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": "색을 입힐 동안 사용할 토큰에 대한 속도 정보 제공자를 선택한다. Drag Ruler는 기본 기능을 제공하며 올바르게 구성된 경우 모든 게임 시스템에서 작동하는 일반적 속도 공급자를 제공한다. 게임 시스템과 설치된 모듈을 통해 더 많은 속도 공급자를 이용할 수 있다. 일반 속도 공급자와는 다른 속도 공급자를 선택하면 사용 중인 게임 시스템의 규칙에 더 잘 통합될 수 있다.아래 옵션은 여기에서 선택한 속도 공급자에 따라 다르다.",
|
|
||||||
"choices": {
|
|
||||||
"module": "모듈 {name}",
|
|
||||||
"native": "포괄적",
|
|
||||||
"system": "시스템 {name}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
{
|
|
||||||
"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}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
After Width: | Height: | Size: 689 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 775 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 603 KiB |
|
After Width: | Height: | Size: 503 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
@@ -2,9 +2,9 @@
|
|||||||
"name": "drag-ruler",
|
"name": "drag-ruler",
|
||||||
"title": "Drag Ruler",
|
"title": "Drag Ruler",
|
||||||
"description": "When dragging a token displays a ruler showing how far you've moved that token.",
|
"description": "When dragging a token displays a ruler showing how far you've moved that token.",
|
||||||
"version": "1.11.2",
|
"version": "0.0.1",
|
||||||
"minimumCoreVersion" : "9.245",
|
"minimumCoreVersion" : "0.7.9",
|
||||||
"compatibleCoreVersion" : "9",
|
"compatibleCoreVersion" : "0.7.9",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "Manuel Vögele",
|
"name": "Manuel Vögele",
|
||||||
@@ -13,62 +13,18 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"esmodules": [
|
"esmodules": [
|
||||||
"src/libwrapper_shim.js",
|
"src/main.js"
|
||||||
"src/main.js",
|
|
||||||
"src/socket.js"
|
|
||||||
],
|
|
||||||
"templates": [
|
|
||||||
"speed_provider_settings.html"
|
|
||||||
],
|
],
|
||||||
"languages": [
|
"languages": [
|
||||||
{
|
|
||||||
"lang": "de",
|
|
||||||
"name": "Deutsch",
|
|
||||||
"path": "lang/de.json"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"lang": "en",
|
"lang": "en",
|
||||||
"name": "English",
|
"name": "English",
|
||||||
"path": "lang/en.json"
|
"path": "lang/en.json"
|
||||||
},
|
|
||||||
{
|
|
||||||
"lang": "es",
|
|
||||||
"name": "Español",
|
|
||||||
"path": "lang/es.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"lang": "fr",
|
|
||||||
"name": "Français",
|
|
||||||
"path": "lang/fr.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"lang": "ja",
|
|
||||||
"name": "日本語",
|
|
||||||
"path": "lang/ja.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"lang": "ko",
|
|
||||||
"name": "한국어",
|
|
||||||
"path": "lang/ko.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",
|
"url": "https://github.com/manuelVo/foundryvtt-drag-ruler",
|
||||||
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/v1.11.2.zip",
|
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/master.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",
|
"bugs": "https://github.com/manuelVo/foundryvtt-drag-ruler/issues"
|
||||||
"bugs": "https://github.com/manuelVo/foundryvtt-drag-ruler/issues",
|
|
||||||
"allowBugReporter": true
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +1,37 @@
|
|||||||
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 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 providerSetting = game.settings.settings.get("drag-ruler.speedProvider")
|
||||||
let providerInstance
|
|
||||||
if (speedProvider.prototype instanceof SpeedProvider) {
|
|
||||||
providerInstance = new speedProvider(id)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.warn(`Drag Ruler | The ${type} '${module.id}' uses the old, deprecated version of the Drag Ruler API. ` +
|
|
||||||
"That old API will be removed in a future Drag Ruler version. " +
|
|
||||||
`Please update the ${type} ${module.id} to stay compatible with future Drag Ruler versions.`);
|
|
||||||
speedProvider.id = id
|
|
||||||
speedProvider.usesRuler = () => true
|
|
||||||
providerInstance = speedProvider
|
|
||||||
}
|
|
||||||
setupProvider(providerInstance)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupProvider(speedProvider) {
|
// Add the registered module to the settings entry
|
||||||
if (speedProvider instanceof SpeedProvider) {
|
providerSetting.config = true
|
||||||
const unreachableColor = {id: "unreachable", default: speedProvider.defaultUnreachableColor, name: "drag-ruler.settings.speedProviderSettings.color.unreachable.name"}
|
const moduleName = module.data.title
|
||||||
for (const color of speedProvider.colors.concat([unreachableColor])) {
|
const typeTitle = game.i18n.localize(`drag-ruler.settings.speedProvider.choices.${type}`)
|
||||||
game.settings.register(settingsKey, `speedProviders.${speedProvider.id}.color.${color.id}`, {
|
providerSetting.choices[`${type}.${module.id}`] = `${typeTitle} ${moduleName}`
|
||||||
config: false,
|
availableSpeedProviders[`${type}.${module.id}`] = speedProvider
|
||||||
scope: "client",
|
providerSetting.default = getDefaultSpeedProvider()
|
||||||
type: Number,
|
|
||||||
default: color.default,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
for (const setting of speedProvider.settings) {
|
|
||||||
setting.config = false
|
|
||||||
game.settings.register(settingsKey, `speedProviders.${speedProvider.id}.setting.${setting.id}`, setting)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
availableSpeedProviders[speedProvider.id] = speedProvider
|
|
||||||
game.settings.settings.get("drag-ruler.speedProvider").default = getDefaultSpeedProvider()
|
|
||||||
updateSpeedProvider()
|
updateSpeedProvider()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaultSpeedProvider() {
|
function getDefaultSpeedProvider() {
|
||||||
const providerIds = Object.keys(availableSpeedProviders)
|
const providerSetting = game.settings.settings.get("drag-ruler.speedProvider")
|
||||||
|
const settingKeys = Object.keys(providerSetting.choices)
|
||||||
// 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 = settingKeys.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 = settingKeys.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 settingKeys[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateSpeedProvider() {
|
export function updateSpeedProvider() {
|
||||||
@@ -70,74 +40,8 @@ export function updateSpeedProvider() {
|
|||||||
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 setCurrentSpeedProvider(newSpeedProvider) {
|
||||||
const genericSpeedProviderInstance = new GenericSpeedProvider("native")
|
currentSpeedProvider = newSpeedProvider
|
||||||
setupProvider(genericSpeedProviderInstance)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRangesFromSpeedProvider(token) {
|
|
||||||
try {
|
|
||||||
if (currentSpeedProvider instanceof Function)
|
|
||||||
return currentSpeedProvider(token, 0x00FF00)
|
|
||||||
const ranges = currentSpeedProvider.getRanges(token)
|
|
||||||
for (const range of ranges) {
|
|
||||||
range.color = game.settings.get(settingsKey, `speedProviders.${currentSpeedProvider.id}.color.${range.color}`)
|
|
||||||
}
|
|
||||||
return ranges
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUnreachableColorFromSpeedProvider() {
|
|
||||||
if (currentSpeedProvider instanceof Function)
|
|
||||||
return 0xFF0000
|
|
||||||
try {
|
|
||||||
return game.settings.get(settingsKey, `speedProviders.${currentSpeedProvider.id}.color.unreachable`)
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
return 0xFF0000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCostFromSpeedProvider(token, area, options) {
|
|
||||||
try {
|
|
||||||
if (currentSpeedProvider instanceof Function) {
|
|
||||||
return SpeedProvider.prototype.getCostForStep.call(undefined, token, area, options);
|
|
||||||
}
|
|
||||||
return currentSpeedProvider.getCostForStep(token, area, options);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getColorForDistanceAndToken(distance, token, ranges=null) {
|
|
||||||
if (!ranges) {
|
|
||||||
ranges = getRangesFromSpeedProvider(token);
|
|
||||||
}
|
|
||||||
if (ranges.length === 0)
|
|
||||||
return null;
|
|
||||||
const currentRange = ranges.reduce((minRange, currentRange) => {
|
|
||||||
if (distance <= currentRange.range && currentRange.range < minRange.range)
|
|
||||||
return currentRange;
|
|
||||||
return minRange;
|
|
||||||
}, {range: Infinity, color: getUnreachableColorFromSpeedProvider()});
|
|
||||||
return currentRange.color;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMovedDistanceFromToken(token) {
|
|
||||||
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active;
|
|
||||||
const history = getMovementHistory(token);
|
|
||||||
const segments = Ruler.dragRulerGetRaysFromWaypoints(history, {x: token.x, y: token.y}).map(ray => {return {ray}});
|
|
||||||
const shape = getTokenShape(token);
|
|
||||||
const distances = measureDistances(segments, token, shape, {enableTerrainRuler: terrainRulerAvailable});
|
|
||||||
// Sum up the distances
|
|
||||||
return distances.reduce((acc, val) => acc + val, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerModule(moduleId, speedProvider) {
|
export function registerModule(moduleId, speedProvider) {
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
import {getCostFromSpeedProvider} from "./api.js";
|
|
||||||
import {settingsKey} from "./settings.js";
|
|
||||||
import {getAreaFromPositionAndShape, highlightTokenShape} from "./util.js";
|
|
||||||
|
|
||||||
export function getHexSizeSupportTokenGridCenter(token) {
|
|
||||||
const tokenCenterOffset = CONFIG.hexSizeSupport.getCenterOffset(token)
|
|
||||||
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 = this.dragRulerGetColorForDistance(startDistance + space.distance);
|
|
||||||
highlightTokenShape.call(this, space, tokenShape, color, alpha)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function measureDistances(segments, entity, shape, options={}) {
|
|
||||||
const opts = duplicate(options)
|
|
||||||
if (canvas.grid.diagonalRule === "EUCL") {
|
|
||||||
opts.ignoreGrid = true;
|
|
||||||
opts.gridSpaes = false;
|
|
||||||
}
|
|
||||||
if (opts.enableTerrainRuler) {
|
|
||||||
opts.gridSpaces = true;
|
|
||||||
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, costOptions={}) => { return getCostFromSpeedProvider(entity, getAreaFromPositionAndShape({x, y}, shape), costOptions); }
|
|
||||||
if (previousSegments.length > 0)
|
|
||||||
opts.terrainRulerInitialState = previousSegments[previousSegments.length - 1].ray.dragRulerFinalState;
|
|
||||||
return distances.concat(terrainRuler.measureDistances(newSegments, opts));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// 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
|
|
||||||
if(!opts.ignoreGrid) {
|
|
||||||
opts.gridSpaces = true;
|
|
||||||
}
|
|
||||||
return canvas.grid.measureDistances(segments, opts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
// 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]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getGridPositionFromPixelsObj(o) {
|
|
||||||
const r = {};
|
|
||||||
[r.x, r.y] = getGridPositionFromPixels(o.x, o.y);
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPixelsFromGridPositionObj(o) {
|
|
||||||
const r = {};
|
|
||||||
[r.x, r.y] = getPixelsFromGridPosition(o.x, o.y);
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCenterFromGridPositionObj(o) {
|
|
||||||
const r = getPixelsFromGridPositionObj(o);
|
|
||||||
[r.x, r.y] = canvas.grid.getCenter(r.x, r.y);
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
@@ -1,369 +0,0 @@
|
|||||||
import {highlightMeasurementTerrainRuler, measureDistances} from "./compatibility.js";
|
|
||||||
import {getGridPositionFromPixels, getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
|
|
||||||
import {Line} from "./geometry.js";
|
|
||||||
import {disableSnap, moveWithoutAnimation} from "./keybindings.js";
|
|
||||||
import {trackRays} from "./movement_tracking.js"
|
|
||||||
import {findPath, isPathfindingEnabled} from "./pathfinding.js";
|
|
||||||
import {settingsKey} from "./settings.js";
|
|
||||||
import {recalculate} from "./socket.js";
|
|
||||||
import {applyTokenSizeOffset, enumeratedZip, getSnapPointForEntity, getSnapPointForToken, getSnapPointForTokenObj, getTokenShape, highlightTokenShape, sum} from "./util.js";
|
|
||||||
|
|
||||||
// This is a modified version of Ruler.moveToken from foundry 0.7.9
|
|
||||||
export async function moveEntities(draggedEntity, selectedEntities) {
|
|
||||||
let wasPaused = game.paused;
|
|
||||||
if (wasPaused && !game.user.isGM) {
|
|
||||||
ui.notifications.warn(game.i18n.localize("GAME.PausedWarning"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!this.visible || !this.destination) return false;
|
|
||||||
if (!draggedEntity) return;
|
|
||||||
|
|
||||||
// Wait until all scheduled measurements are done
|
|
||||||
await this.deferredMeasurementPromise;
|
|
||||||
|
|
||||||
// Get the movement rays and check collision along each Ray
|
|
||||||
// These rays are center-to-center for the purposes of collision checking
|
|
||||||
const rays = this.constructor.dragRulerGetRaysFromWaypoints(this.waypoints, this.destination);
|
|
||||||
if (!game.user.isGM && draggedEntity instanceof Token) {
|
|
||||||
const hasCollision = selectedEntities.some(token => {
|
|
||||||
const offset = calculateEntityOffset(token, draggedEntity);
|
|
||||||
const offsetRays = rays.filter(ray => !ray.isPrevious).map(ray => applyOffsetToRay(ray, offset))
|
|
||||||
return offsetRays.some(r => canvas.walls.checkCollision(r));
|
|
||||||
})
|
|
||||||
if (hasCollision) {
|
|
||||||
ui.notifications.error(game.i18n.localize("ERROR.TokenCollide"));
|
|
||||||
this._endMeasurement();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute the movement path.
|
|
||||||
// Transform each center-to-center ray into a top-left to top-left ray using the prior token offsets.
|
|
||||||
this._state = Ruler.STATES.MOVING;
|
|
||||||
await animateEntities.call(this, selectedEntities, draggedEntity, rays, wasPaused);
|
|
||||||
|
|
||||||
// Once all animations are complete we can clear the ruler
|
|
||||||
if (this.draggedEntity?.id === draggedEntity.id)
|
|
||||||
this._endMeasurement();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a modified version code extracted from Ruler.moveToken from foundry 0.7.9
|
|
||||||
async function animateEntities(entities, draggedEntity, draggedRays, wasPaused) {
|
|
||||||
const newRays = draggedRays.filter(r => !r.isPrevious);
|
|
||||||
const entityAnimationData = entities.map(entity => {
|
|
||||||
const entityOffset = calculateEntityOffset(entity, draggedEntity);
|
|
||||||
const offsetRays = newRays.map(ray => applyOffsetToRay(ray, entityOffset));
|
|
||||||
|
|
||||||
// Determine offset relative to the Token top-left.
|
|
||||||
// This is important so we can position the token relative to the ruler origin for non-1x1 tokens.
|
|
||||||
const firstWaypoint = this.waypoints.find(w => !w.isPrevious);
|
|
||||||
const origin = [firstWaypoint.x + entityOffset.x, firstWaypoint.y + entityOffset.y];
|
|
||||||
let dx, dy;
|
|
||||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
|
||||||
dx = entity.data.x - origin[0];
|
|
||||||
dy = entity.data.y - origin[1];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
dx = entity.data.x - origin[0];
|
|
||||||
dy = entity.data.y - origin[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {entity, rays: offsetRays, dx, dy};
|
|
||||||
});
|
|
||||||
|
|
||||||
const isToken = draggedEntity instanceof Token;
|
|
||||||
const animate = isToken && !moveWithoutAnimation;
|
|
||||||
const startWaypoint = animate ? 0 : entityAnimationData[0].rays.length - 1;
|
|
||||||
|
|
||||||
// This is a flag of the "Monk's Active Tile Triggers" module that signals that the movement should be cancelled early
|
|
||||||
this.cancelMovement = false;
|
|
||||||
|
|
||||||
for (let i = startWaypoint;i < entityAnimationData[0].rays.length; i++) {
|
|
||||||
if (!wasPaused && game.paused) break;
|
|
||||||
const entityPaths = entityAnimationData.map(({entity, rays, dx, dy}) => {
|
|
||||||
const ray = rays[i];
|
|
||||||
const dest = [ray.B.x, ray.B.y];
|
|
||||||
const path = new Ray({x: entity.x, y: entity.y}, {x: dest[0] + dx, y: dest[1] + dy});
|
|
||||||
return {entity, path};
|
|
||||||
});
|
|
||||||
const updates = entityPaths.map(({entity, path}) => {
|
|
||||||
return {x: path.B.x, y: path.B.y, _id: entity.id};
|
|
||||||
});
|
|
||||||
await draggedEntity.scene.updateEmbeddedDocuments(draggedEntity.constructor.embeddedName, updates, {animate});
|
|
||||||
if (animate)
|
|
||||||
await Promise.all(entityPaths.map(({entity, path}) => entity.animateMovement(path)));
|
|
||||||
|
|
||||||
// This is a flag of the "Monk's Active Tile Triggers" module that signals that the movement should be cancelled early
|
|
||||||
if (this.cancelMovement) {
|
|
||||||
entityAnimationData.forEach(ead => ead.rays = ead.rays.slice(0, i + 1));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isToken)
|
|
||||||
trackRays(entities, entityAnimationData.map(({rays}) => rays)).then(() => recalculate(entities));
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateEntityOffset(entityA, entityB) {
|
|
||||||
return {x: entityA.data.x - entityB.data.x, y: entityA.data.y - entityB.data.y};
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyOffsetToRay(ray, offset) {
|
|
||||||
const newRay = new Ray({x: ray.A.x + offset.x, y: ray.A.y + offset.y}, {x: ray.B.x + offset.x, y: ray.B.y + offset.y});
|
|
||||||
newRay.isPrevious = ray.isPrevious;
|
|
||||||
return newRay;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a modified version of Ruler._onMouseMove from foundry 0.7.9
|
|
||||||
export function onMouseMove(event) {
|
|
||||||
if (this._state === Ruler.STATES.MOVING) return;
|
|
||||||
|
|
||||||
// Extract event data
|
|
||||||
const destination = {x: event.data.destination.x + this.rulerOffset.x, y: event.data.destination.y + this.rulerOffset.y}
|
|
||||||
|
|
||||||
// Hide any existing Token HUD
|
|
||||||
canvas.hud.token.clear();
|
|
||||||
delete event.data.hudState;
|
|
||||||
|
|
||||||
// Draw measurement updates
|
|
||||||
scheduleMeasurement.call(this, destination, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleMeasurement(destination, event) {
|
|
||||||
const measurementInterval = 50;
|
|
||||||
const mt = event._measureTime || 0;
|
|
||||||
const originalEvent = event.data.originalEvent;
|
|
||||||
if (Date.now() - mt > measurementInterval) {
|
|
||||||
this.measure(destination, {snap: !disableSnap});
|
|
||||||
event._measureTime = Date.now();
|
|
||||||
this._state = Ruler.STATES.MEASURING;
|
|
||||||
cancelScheduledMeasurement.call(this);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.deferredMeasurementData = {destination, event};
|
|
||||||
if (!this.deferredMeasurementTimeout) {
|
|
||||||
this.deferredMeasurementPromise = new Promise((resolve, reject) => this.deferredMeasurementResolve = resolve);
|
|
||||||
this.deferredMeasurementTimeout = window.setTimeout(() => scheduleMeasurement.call(this, this.deferredMeasurementData.destination, this.deferredMeasurementData.event), measurementInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cancelScheduledMeasurement() {
|
|
||||||
window.clearTimeout(this.deferredMeasurementTimeout);
|
|
||||||
this.deferredMeasurementTimeout = undefined;
|
|
||||||
this.deferredMeasurementResolve?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a modified version of Ruler.measure form foundry 0.7.9
|
|
||||||
export function measure(destination, options={}) {
|
|
||||||
|
|
||||||
const isToken = this.draggedEntity instanceof Token;
|
|
||||||
if (isToken && !this.draggedEntity.isVisible)
|
|
||||||
return []
|
|
||||||
|
|
||||||
options.snap = options.snap ?? !disableSnap;
|
|
||||||
|
|
||||||
if (options.snap) {
|
|
||||||
destination = getSnapPointForEntity(destination.x, destination.y, this.draggedEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dragRulerRemovePathfindingWaypoints();
|
|
||||||
|
|
||||||
if (isToken && isPathfindingEnabled()) {
|
|
||||||
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
|
|
||||||
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 pathUntilSpace = previousSegments.concat([{ray: new Ray(ray.A, {x: xg, y: yg})}]);
|
|
||||||
const distance = sum(canvas.grid.measureDistances(pathUntilSpace, {gridSpaces: true}));
|
|
||||||
const color = this.dragRulerGetColorForDistance(distance);
|
|
||||||
const snapPoint = getSnapPointForToken(...canvas.grid.getTopLeft(x, y), this.draggedEntity);
|
|
||||||
const [snapX, snapY] = getGridPositionFromPixels(snapPoint.x + 1, snapPoint.y + 1);
|
|
||||||
|
|
||||||
prior = [x1, y1];
|
|
||||||
|
|
||||||
// If the positions are not neighbors, also highlight their halfway point
|
|
||||||
if (i > 0 && !canvas.grid.isNeighbor(x0, y0, x1, y1)) {
|
|
||||||
let th = tMax[i - 1] - (0.5 / nMax);
|
|
||||||
let {x, y} = ray.project(th);
|
|
||||||
let [x1h, y1h] = canvas.grid.grid.getGridPositionFromPixels(x, y);
|
|
||||||
let [xgh, ygh] = canvas.grid.grid.getPixelsFromGridPosition(x1h, y1h);
|
|
||||||
const pathUntilSpace = previousSegments.concat([{ray: new Ray(ray.A, {x: xgh, y: ygh})}]);
|
|
||||||
const distance = sum(canvas.grid.measureDistances(pathUntilSpace, {gridSpaces: true}));
|
|
||||||
const color = this.dragRulerGetColorForDistance(distance);
|
|
||||||
const snapPoint = getSnapPointForToken(...canvas.grid.getTopLeft(x, y), this.draggedEntity);
|
|
||||||
const [snapX, snapY] = getGridPositionFromPixels(snapPoint.x + 1, snapPoint.y + 1);
|
|
||||||
highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color, alpha);
|
|
||||||
}
|
|
||||||
|
|
||||||
highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color, alpha);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
export class Line {
|
|
||||||
constructor(m, b) {
|
|
||||||
this.m = m;
|
|
||||||
this.b = b;
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromPoints(p1, p2) {
|
|
||||||
// Bring line into y=mx+b form
|
|
||||||
const m = (p1.y - p2.y) / (p1.x - p2.x);
|
|
||||||
const b = p1.y - m * p1.x;
|
|
||||||
return new Line(m, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
get isVertical() {
|
|
||||||
return !isFinite(this.m);
|
|
||||||
}
|
|
||||||
|
|
||||||
calcY(x) {
|
|
||||||
return this.m * x + this.b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import {settingsKey} from "./settings.js";
|
|
||||||
import {getMeasurePosition, setSnapParameterOnOptions} from "./util.js";
|
|
||||||
|
|
||||||
export let disableSnap = false;
|
|
||||||
export let moveWithoutAnimation = false;
|
|
||||||
export let togglePathfinding = false;
|
|
||||||
|
|
||||||
export function registerKeybindings() {
|
|
||||||
game.keybindings.register(settingsKey, "cancelDrag", {
|
|
||||||
name: "drag-ruler.keybindings.cancelDrag",
|
|
||||||
onDown: cancelDrag,
|
|
||||||
uneditable: [{
|
|
||||||
key: "Escape",
|
|
||||||
}],
|
|
||||||
precedence: -1,
|
|
||||||
});
|
|
||||||
|
|
||||||
game.keybindings.register(settingsKey, "createWaypoint", {
|
|
||||||
name: "drag-ruler.keybindings.createWaypoint",
|
|
||||||
onDown: handleCreateWaypoint,
|
|
||||||
editable: [{
|
|
||||||
key: "Space"
|
|
||||||
}],
|
|
||||||
precedence: -1,
|
|
||||||
});
|
|
||||||
|
|
||||||
game.keybindings.register(settingsKey, "deleteWaypoint", {
|
|
||||||
name: "drag-ruler.keybindings.deleteWaypoint",
|
|
||||||
onDown: handleDeleteWaypoint,
|
|
||||||
precedence: -1,
|
|
||||||
});
|
|
||||||
|
|
||||||
game.keybindings.register(settingsKey, "disableSnap", {
|
|
||||||
name: "drag-ruler.keybindings.disableSnap.name",
|
|
||||||
hint: "drag-ruler.keybindings.disableSnap.hint",
|
|
||||||
onDown: handleDisableSnap,
|
|
||||||
onUp: handleDisableSnap,
|
|
||||||
editable: [{
|
|
||||||
key: "ShiftLeft",
|
|
||||||
}],
|
|
||||||
precedence: -1,
|
|
||||||
});
|
|
||||||
|
|
||||||
game.keybindings.register(settingsKey, "moveWithoutAnimation", {
|
|
||||||
name: "drag-ruler.keybindings.moveWithoutAnimation.name",
|
|
||||||
hint: "drag-ruler.keybindings.moveWithoutAnimation.hint",
|
|
||||||
onDown: handleMoveWithoutAnimation,
|
|
||||||
onUp: handleMoveWithoutAnimation,
|
|
||||||
editable: [{
|
|
||||||
key: "AltLeft",
|
|
||||||
}],
|
|
||||||
precedence: -1,
|
|
||||||
});
|
|
||||||
|
|
||||||
game.keybindings.register(settingsKey, "togglePathfinding", {
|
|
||||||
name: "drag-ruler.keybindings.togglePathfinding.name",
|
|
||||||
hint: "drag-ruler.keybindings.togglePathfinding.hint",
|
|
||||||
onDown: handleTogglePathfinding,
|
|
||||||
onUp: handleTogglePathfinding,
|
|
||||||
precedence: -1,
|
|
||||||
restricted: !game.settings.get(settingsKey, "allowPathfinding"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDeleteWaypoint() {
|
|
||||||
const ruler = canvas.controls.ruler;
|
|
||||||
if (!ruler?.draggedEntity)
|
|
||||||
return false;
|
|
||||||
ruler.dragRulerDeleteWaypoint();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCreateWaypoint() {
|
|
||||||
const ruler = canvas.controls.ruler;
|
|
||||||
// .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
|
|
||||||
if (!ruler?.draggedEntity)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
let options = {};
|
|
||||||
setSnapParameterOnOptions(ruler, options);
|
|
||||||
|
|
||||||
if (ruler._state === Ruler.STATES.INACTIVE) {
|
|
||||||
ruler.dragRulerStart(options);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
ruler.dragRulerAddWaypoint(getMeasurePosition(), options);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelDrag() {
|
|
||||||
const ruler = canvas.controls.ruler;
|
|
||||||
if (!ruler?.draggedEntity)
|
|
||||||
return false;
|
|
||||||
ruler.dragRulerAbortDrag();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDisableSnap(event) {
|
|
||||||
disableSnap = !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});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMoveWithoutAnimation(event) {
|
|
||||||
moveWithoutAnimation = !event.up;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleTogglePathfinding(event) {
|
|
||||||
togglePathfinding = !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});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
// Copyright © 2021 fvtt-lib-wrapper Rui Pinheiro
|
|
||||||
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// A shim for the libWrapper library
|
|
||||||
export let libWrapper = undefined;
|
|
||||||
|
|
||||||
export const VERSIONS = [1,11,0];
|
|
||||||
export const TGT_SPLIT_RE = new RegExp("([^.[]+|\\[('([^'\\\\]|\\\\.)+?'|\"([^\"\\\\]|\\\\.)+?\")\\])", 'g');
|
|
||||||
export const TGT_CLEANUP_RE = new RegExp("(^\\['|'\\]$|^\\[\"|\"\\]$)", 'g');
|
|
||||||
|
|
||||||
// Main shim code
|
|
||||||
Hooks.once('init', () => {
|
|
||||||
// Check if the real module is already loaded - if so, use it
|
|
||||||
if(globalThis.libWrapper && !(globalThis.libWrapper.is_fallback ?? true)) {
|
|
||||||
libWrapper = globalThis.libWrapper;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback implementation
|
|
||||||
libWrapper = class {
|
|
||||||
static get is_fallback() { return true };
|
|
||||||
|
|
||||||
static get WRAPPER() { return 'WRAPPER' };
|
|
||||||
static get MIXED() { return 'MIXED' };
|
|
||||||
static get OVERRIDE() { return 'OVERRIDE' };
|
|
||||||
|
|
||||||
static register(package_id, target, fn, type="MIXED", {chain=undefined}={}) {
|
|
||||||
const is_setter = target.endsWith('#set');
|
|
||||||
target = !is_setter ? target : target.slice(0, -4);
|
|
||||||
const split = target.match(TGT_SPLIT_RE).map((x)=>x.replace(/\\(.)/g, '$1').replace(TGT_CLEANUP_RE,''));
|
|
||||||
const root_nm = split.splice(0,1)[0];
|
|
||||||
|
|
||||||
let obj, fn_name;
|
|
||||||
if(split.length == 0) {
|
|
||||||
obj = globalThis;
|
|
||||||
fn_name = root_nm;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const _eval = eval;
|
|
||||||
fn_name = split.pop();
|
|
||||||
obj = split.reduce((x,y)=>x[y], globalThis[root_nm] ?? _eval(root_nm));
|
|
||||||
}
|
|
||||||
|
|
||||||
let iObj = obj;
|
|
||||||
let descriptor = null;
|
|
||||||
while(iObj) {
|
|
||||||
descriptor = Object.getOwnPropertyDescriptor(iObj, fn_name);
|
|
||||||
if(descriptor) break;
|
|
||||||
iObj = Object.getPrototypeOf(iObj);
|
|
||||||
}
|
|
||||||
if(!descriptor || descriptor?.configurable === false) throw `libWrapper Shim: '${target}' does not exist, could not be found, or has a non-configurable descriptor.`;
|
|
||||||
|
|
||||||
let original = null;
|
|
||||||
const wrapper = (chain ?? (type.toUpperCase?.() != 'OVERRIDE' && type != 3)) ? function() { return fn.call(this, original.bind(this), ...arguments); } : function() { return fn.apply(this, arguments); };
|
|
||||||
|
|
||||||
if(!is_setter) {
|
|
||||||
if(descriptor.value) {
|
|
||||||
original = descriptor.value;
|
|
||||||
descriptor.value = wrapper;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
original = descriptor.get;
|
|
||||||
descriptor.get = wrapper;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if(!descriptor.set) throw `libWrapper Shim: '${target}' does not have a setter`;
|
|
||||||
original = descriptor.set;
|
|
||||||
descriptor.set = wrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
descriptor.configurable = true;
|
|
||||||
Object.defineProperty(obj, fn_name, descriptor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,244 +1,178 @@
|
|||||||
"use strict"
|
"use strict"
|
||||||
|
|
||||||
import {getColorForDistanceAndToken, getMovedDistanceFromToken, getRangesFromSpeedProvider, initApi, registerModule, registerSystem} from "./api.js";
|
import {availableSpeedProviders, currentSpeedProvider, registerModule, registerSystem, setCurrentSpeedProvider} from "./api.js"
|
||||||
import {checkDependencies, getHexSizeSupportTokenGridCenter} from "./compatibility.js";
|
import {registerSettings, settingsKey} from "./settings.js"
|
||||||
import {moveEntities, onMouseMove} from "./foundry_imports.js"
|
|
||||||
import {disableSnap, registerKeybindings} from "./keybindings.js";
|
|
||||||
import {libWrapper} from "./libwrapper_shim.js";
|
|
||||||
import {performMigrations} from "./migration.js"
|
|
||||||
import {removeLastHistoryEntryIfAt, resetMovementHistory} from "./movement_tracking.js";
|
|
||||||
import {extendRuler} from "./ruler.js";
|
|
||||||
import {registerSettings, RightClickAction, settingsKey} from "./settings.js"
|
|
||||||
import {recalculate} from "./socket.js";
|
|
||||||
import {SpeedProvider} from "./speed_provider.js"
|
|
||||||
import {setSnapParameterOnOptions} from "./util.js";
|
|
||||||
|
|
||||||
CONFIG.debug.dragRuler = false;
|
|
||||||
export let debugGraphics = undefined;
|
|
||||||
|
|
||||||
Hooks.once("init", () => {
|
Hooks.once("init", () => {
|
||||||
registerSettings()
|
registerSettings()
|
||||||
registerKeybindings()
|
hookTokenDragHandlers()
|
||||||
initApi()
|
hookRulerHandlers()
|
||||||
hookDragHandlers(Token);
|
patchRulerMeasure()
|
||||||
hookDragHandlers(MeasuredTemplate);
|
patchRulerHighlightMeasurement()
|
||||||
libWrapper.register("drag-ruler", "TokenLayer.prototype.undoHistory", tokenLayerUndoHistory, "WRAPPER");
|
|
||||||
|
|
||||||
extendRuler();
|
availableSpeedProviders["native"] = nativeSpeedProvider
|
||||||
|
setCurrentSpeedProvider(nativeSpeedProvider)
|
||||||
|
|
||||||
window.dragRuler = {
|
window.dragRuler = {
|
||||||
getColorForDistanceAndToken,
|
getColorForDistance,
|
||||||
getMovedDistanceFromToken,
|
|
||||||
registerModule,
|
registerModule,
|
||||||
registerSystem,
|
registerSystem
|
||||||
recalculate,
|
|
||||||
resetMovementHistory,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
Hooks.once("ready", () => {
|
Hooks.once("ready", () => {
|
||||||
performMigrations()
|
Hooks.callAll("dragRuler.ready")
|
||||||
checkDependencies();
|
|
||||||
Hooks.callAll("dragRuler.ready", SpeedProvider)
|
|
||||||
if (CONFIG.debug.dragRuler)
|
|
||||||
debugGraphics = canvas.controls.addChild(new PIXI.Container());
|
|
||||||
})
|
})
|
||||||
|
|
||||||
Hooks.on("canvasReady", () => {
|
Hooks.on("canvasReady", () => {
|
||||||
canvas.controls.rulers.children.forEach(ruler => {
|
canvas.controls.ruler.draggedToken = null
|
||||||
ruler.draggedEntity = null;
|
Object.defineProperty(canvas.controls.ruler, "isDragRuler", {
|
||||||
Object.defineProperty(ruler, "isDragRuler", {
|
|
||||||
get: function isDragRuler() {
|
get: function isDragRuler() {
|
||||||
return Boolean(this.draggedEntity) && this._state !== Ruler.STATES.INACTIVE;
|
return Boolean(this.draggedToken) // If draggedToken is set this is a drag ruler
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
Hooks.on("getCombatTrackerEntryContext", function (html, menu) {
|
function hookTokenDragHandlers() {
|
||||||
const entry = {
|
const originalDragLeftStartHandler = Token.prototype._onDragLeftStart
|
||||||
name: "drag-ruler.resetMovementHistory",
|
Token.prototype._onDragLeftStart = function(event) {
|
||||||
icon: '<i class="fas fa-undo-alt"></i>',
|
originalDragLeftStartHandler.call(this, event)
|
||||||
callback: li => resetMovementHistory(ui.combat.viewed, li.data('combatant-id')),
|
onTokenLeftDragStart.call(this, event)
|
||||||
};
|
}
|
||||||
menu.splice(1, 0, entry);
|
|
||||||
});
|
|
||||||
|
|
||||||
function forwardIfUnahndled(newFn) {
|
const originalDragLeftMoveHandler = Token.prototype._onDragLeftMove
|
||||||
return function(oldFn, ...args) {
|
Token.prototype._onDragLeftMove = function (event) {
|
||||||
const eventHandled = newFn(...args);
|
originalDragLeftMoveHandler.call(this, event)
|
||||||
|
onTokenLeftDragMove.call(this, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalDragLeftDropHandler = Token.prototype._onDragLeftDrop
|
||||||
|
Token.prototype._onDragLeftDrop = function (event) {
|
||||||
|
const eventHandled = onTokenDragLeftDrop.call(this, event)
|
||||||
if (!eventHandled)
|
if (!eventHandled)
|
||||||
oldFn(...args);
|
originalDragLeftDropHandler.call(this, event)
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function hookDragHandlers(entityType) {
|
|
||||||
const entityName = entityType.name
|
|
||||||
libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftStart`, onEntityLeftDragStart, "WRAPPER");
|
|
||||||
if (entityType === Token)
|
|
||||||
libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftMove`, onEntityLeftDragMoveSnap, "WRAPPER");
|
|
||||||
else
|
|
||||||
libWrapper.register("drag-ruler", `${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) {
|
|
||||||
const historyEntry = this.history[this.history.length - 1];
|
|
||||||
const returnValue = await wrapped();
|
|
||||||
if (historyEntry.type === "update") {
|
|
||||||
for (const entry of historyEntry.data) {
|
|
||||||
const token = canvas.tokens.get(entry._id);
|
|
||||||
removeLastHistoryEntryIfAt(token, entry.x, entry.y);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return returnValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onEntityLeftDragStart(wrapped, event) {
|
const originalDragLeftCancelHandler = Token.prototype._onDragLeftCancel
|
||||||
wrapped(event);
|
Token.prototype._onDragLeftCancel = function (event) {
|
||||||
const isToken = this instanceof Token;
|
const eventHandled = onTokenDragLeftCancel.call(this, event)
|
||||||
const ruler = canvas.controls.ruler
|
if (!eventHandled)
|
||||||
ruler.draggedEntity = this;
|
originalDragLeftCancelHandler.call(this, event)
|
||||||
let entityCenter;
|
|
||||||
if (isToken && canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(this))
|
|
||||||
entityCenter = getHexSizeSupportTokenGridCenter(this);
|
|
||||||
else
|
|
||||||
entityCenter = this.center;
|
|
||||||
ruler.rulerOffset = {x: entityCenter.x - event.data.origin.x, y: entityCenter.y - event.data.origin.y};
|
|
||||||
if (game.settings.get(settingsKey, "autoStartMeasurement")) {
|
|
||||||
let options = {};
|
|
||||||
setSnapParameterOnOptions(ruler, options);
|
|
||||||
ruler.dragRulerStart(options, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEntityLeftDragMoveSnap(wrapped, event) {
|
function hookRulerHandlers() {
|
||||||
applyGridlessSnapping.call(this, event);
|
const originalMoveTokenHandler = Ruler.prototype.moveToken
|
||||||
onEntityLeftDragMove.call(this, wrapped, event);
|
Ruler.prototype.moveToken = function (event) {
|
||||||
|
const eventHandled = onRulerMoveToken.call(this, event)
|
||||||
|
if (!eventHandled)
|
||||||
|
return originalMoveTokenHandler.call(this, event)
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEntityLeftDragMove(wrapped, event) {
|
function onTokenLeftDragStart(event) {
|
||||||
wrapped(event);
|
canvas.controls.ruler._onDragStart(event)
|
||||||
const ruler = canvas.controls.ruler
|
canvas.controls.ruler.draggedToken = this
|
||||||
if (ruler.isDragRuler)
|
|
||||||
onMouseMove.call(ruler, event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEntityDragLeftDrop(event) {
|
function onTokenLeftDragMove(event) {
|
||||||
const ruler = canvas.controls.ruler
|
if (canvas.controls.ruler.isDragRuler)
|
||||||
if (!ruler.isDragRuler) {
|
canvas.controls.ruler._onMouseMove(event)
|
||||||
ruler.draggedEntity = undefined;
|
}
|
||||||
|
|
||||||
|
function onTokenDragLeftDrop(event) {
|
||||||
|
if (!canvas.controls.ruler.isDragRuler)
|
||||||
return false
|
return false
|
||||||
}
|
canvas.controls.ruler.draggedToken = null
|
||||||
// When we're dragging a measured template no token will ever be selected,
|
canvas.controls.ruler.moveToken(event)
|
||||||
// resulting in only the dragged template to be moved as would be expected
|
|
||||||
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.draggedEntity);
|
|
||||||
ruler._state = Ruler.STATES.MOVING
|
|
||||||
moveEntities.call(ruler, ruler.draggedEntity, selectedTokens);
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEntityDragLeftCancel(event) {
|
function onTokenDragLeftCancel(event) {
|
||||||
// This function is invoked by right clicking
|
if (!canvas.controls.ruler.isDragRuler)
|
||||||
const ruler = canvas.controls.ruler
|
|
||||||
if (!ruler.draggedEntity || ruler._state === Ruler.STATES.MOVING)
|
|
||||||
return false
|
return false
|
||||||
|
if (canvas.controls.ruler._state === Ruler.STATES.MEASURING) {
|
||||||
const rightClickAction = game.settings.get(settingsKey, "rightClickAction");
|
if (canvas.controls.ruler.waypoints.length > 1) {
|
||||||
let options = {};
|
canvas.controls.ruler._removeWaypoint(canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens), {snap: !event.shiftKey})
|
||||||
setSnapParameterOnOptions(ruler, options);
|
game.user.broadcastActivity({ruler: canvas.controls.ruler})
|
||||||
|
event.preventDefault()
|
||||||
if (ruler._state === Ruler.STATES.INACTIVE) {
|
|
||||||
if (rightClickAction !== RightClickAction.CREATE_WAYPOINT)
|
|
||||||
return false;
|
|
||||||
ruler.dragRulerStart(options);
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
else if (ruler._state === Ruler.STATES.MEASURING) {
|
|
||||||
switch (rightClickAction) {
|
|
||||||
case RightClickAction.CREATE_WAYPOINT:
|
|
||||||
event.preventDefault();
|
|
||||||
ruler.dragRulerAddWaypoint(ruler.destination, options);
|
|
||||||
break;
|
|
||||||
case RightClickAction.DELETE_WAYPOINT:
|
|
||||||
ruler.dragRulerDeleteWaypoint(event, options);
|
|
||||||
break;
|
|
||||||
case RightClickAction.ABORT_DRAG:
|
|
||||||
ruler.dragRulerAbortDrag();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyGridlessSnapping(event) {
|
|
||||||
const ruler = canvas.controls.ruler;
|
|
||||||
if (!game.settings.get(settingsKey, "useGridlessRaster"))
|
|
||||||
return;
|
|
||||||
if (!ruler.isDragRuler)
|
|
||||||
return;
|
|
||||||
if (disableSnap)
|
|
||||||
return;
|
|
||||||
if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const rasterWidth = 35 / canvas.stage.scale.x;
|
|
||||||
const tokenX = event.data.destination.x;
|
|
||||||
const tokenY = event.data.destination.y;
|
|
||||||
const destination = {x: tokenX + ruler.rulerOffset.x, y: tokenY + ruler.rulerOffset.y};
|
|
||||||
const ranges = getRangesFromSpeedProvider(ruler.draggedEntity);
|
|
||||||
|
|
||||||
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active;
|
|
||||||
if (terrainRulerAvailable) {
|
|
||||||
const segments = Ruler.dragRulerGetRaysFromWaypoints(ruler.waypoints, destination).map(ray => {return {ray}});
|
|
||||||
const pinpointDistances = new Map();
|
|
||||||
for (const range of ranges) {
|
|
||||||
pinpointDistances.set(range.range, null);
|
|
||||||
}
|
|
||||||
terrainRuler.measureDistances(segments, {pinpointDistances});
|
|
||||||
const targetDistance = Array.from(pinpointDistances.entries())
|
|
||||||
.filter(([_key, val]) => val)
|
|
||||||
.reduce((value, current) => value[0] > current[0] ? value : current, [0, null]);
|
|
||||||
const rasterLocation = targetDistance[1];
|
|
||||||
if (rasterLocation) {
|
|
||||||
const deltaX = destination.x - rasterLocation.x;
|
|
||||||
const deltaY = destination.y - rasterLocation.y;
|
|
||||||
const rasterDistance = Math.hypot(deltaX, deltaY);
|
|
||||||
if (rasterDistance < rasterWidth) {
|
|
||||||
event.data.destination.x = rasterLocation.x - ruler.rulerOffset.x;
|
|
||||||
event.data.destination.y = rasterLocation.y - ruler.rulerOffset.y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let waypointDistance = 0;
|
canvas.controls.ruler._endMeasurement()
|
||||||
let origin = event.data.origin;
|
canvas.controls.ruler.draggedToken = null
|
||||||
if (ruler.waypoints.length > 1) {
|
return false
|
||||||
const segments = Ruler.dragRulerGetRaysFromWaypoints(ruler.waypoints, destination).map(ray => {return {ray}});
|
|
||||||
origin = segments.pop().ray.A;
|
|
||||||
waypointDistance = canvas.grid.measureDistances(segments).reduce((a, b) => a + b);
|
|
||||||
origin = {x: origin.x - ruler.rulerOffset.x, y: origin.y - ruler.rulerOffset.y};
|
|
||||||
}
|
|
||||||
|
|
||||||
const deltaX = tokenX - origin.x;
|
|
||||||
const deltaY = tokenY - origin.y;
|
|
||||||
const distance = Math.hypot(deltaX, deltaY);
|
|
||||||
// targetRange will be the largest range that's still smaller than distance
|
|
||||||
let targetDistance = ranges
|
|
||||||
.map(range => range.range)
|
|
||||||
.map(range => range - waypointDistance)
|
|
||||||
.map(range => range * canvas.dimensions.size / canvas.dimensions.distance)
|
|
||||||
.filter(range => range < distance)
|
|
||||||
.reduce((a, b) => Math.max(a, b), 0);
|
|
||||||
if (targetDistance) {
|
|
||||||
if (distance < targetDistance + rasterWidth) {
|
|
||||||
event.data.destination.x = origin.x + deltaX * targetDistance / distance;
|
|
||||||
event.data.destination.y = origin.y + deltaY * targetDistance / distance;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRulerMoveToken(event) {
|
||||||
|
if (!this.isDragRuler)
|
||||||
|
return false
|
||||||
|
this._addWaypoint(this.destination)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function strInsertAfter(haystack, needle, strToInsert) {
|
||||||
|
const pos = haystack.indexOf(needle) + needle.length
|
||||||
|
return haystack.slice(0, pos) + strToInsert + haystack.slice(pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
// These patches were written with foundry-0.7.9.js as reference
|
||||||
|
function patchRulerMeasure() {
|
||||||
|
let code = Ruler.prototype.measure.toString()
|
||||||
|
// Remove function signature and closing curly bracket (those are on the first and last line)
|
||||||
|
code = code.slice(code.indexOf("\n"), code.lastIndexOf("\n"))
|
||||||
|
code = strInsertAfter(code, "for ( let [i, d] of distances.entries() ) {\n", "segments[i].startDistance = totalDistance\n")
|
||||||
|
code = strInsertAfter(code, "this._highlightMeasurement(ray", ", s.startDistance")
|
||||||
|
Ruler.prototype.measure = new Function("destination", "{gridSpaces=true}={}", code)
|
||||||
|
}
|
||||||
|
|
||||||
|
function nativeSpeedProvider(token, playercolor) {
|
||||||
|
const speedAttribute = game.settings.get(settingsKey, "speedAttribute")
|
||||||
|
if (!speedAttribute)
|
||||||
|
return []
|
||||||
|
const tokenSpeed = getProperty(token, speedAttribute)
|
||||||
|
if (tokenSpeed === undefined) {
|
||||||
|
console.warn(`Drag Ruler | 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 = game.settings.get(settingsKey, "dashMultiplier")
|
||||||
|
if (!dashMultiplier)
|
||||||
|
return [{range: tokenSpeed, color: playercolor}]
|
||||||
|
return [{range: tokenSpeed, color: playercolor}, {range: tokenSpeed * dashMultiplier, color: 0xFFFF00}]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColorForDistance(startDistance, subDistance) {
|
||||||
|
if (!this.isDragRuler)
|
||||||
|
return this.color
|
||||||
|
const distance = startDistance + subDistance
|
||||||
|
const ranges = currentSpeedProvider(this.draggedToken, this.color)
|
||||||
|
if (ranges.length === 0)
|
||||||
|
return this.color
|
||||||
|
const currentRange = ranges.reduce((minRange, currentRange) => {
|
||||||
|
if (distance <= currentRange.range && currentRange.range < minRange.range)
|
||||||
|
return currentRange
|
||||||
|
return minRange
|
||||||
|
}, {range: Infinity, color: 0xFF0000})
|
||||||
|
return currentRange.color
|
||||||
|
}
|
||||||
|
|
||||||
|
// These patches were written with foundry-0.7.9.js as reference
|
||||||
|
function patchRulerHighlightMeasurement() {
|
||||||
|
let code = Ruler.prototype._highlightMeasurement.toString()
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
import {RightClickAction, settingsKey} from "./settings.js"
|
|
||||||
|
|
||||||
const currentDataVersion = "1.10.0"
|
|
||||||
|
|
||||||
export async function performMigrations() {
|
|
||||||
if (game.user.isGM)
|
|
||||||
await performWorldMigraionts()
|
|
||||||
await performClientMigrations()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function performWorldMigraionts() {
|
|
||||||
let dataVersion = game.settings.get(settingsKey, "dataVersion");
|
|
||||||
|
|
||||||
if (dataVersion === currentDataVersion)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (dataVersion === "fresh install") {
|
|
||||||
game.settings.set(settingsKey, "dataVersion", currentDataVersion)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dataVersion === "1.3.0") {
|
|
||||||
dataVersion = "1.10.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
game.settings.set(settingsKey, "dataVersion", dataVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function performClientMigrations() {
|
|
||||||
let dataVersion = game.settings.get(settingsKey, "clientDataVersion");
|
|
||||||
|
|
||||||
if (dataVersion === "fresh install") {
|
|
||||||
// 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`);
|
|
||||||
if (swapSpacebarRightClick) {
|
|
||||||
game.settings.set(settingsKey, "rightClickAction", RightClickAction.CREATE_WAYPOINT);
|
|
||||||
await game.keybindings.set(settingsKey, "createWaypoint", []);
|
|
||||||
await game.keybindings.set(settingsKey, "deleteWaypoint", [{key: "Space"}]);
|
|
||||||
}
|
|
||||||
// End of migration from unnamed version
|
|
||||||
game.settings.set(settingsKey, "clientDataVersion", currentDataVersion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import {measureDistances} from "./compatibility.js";
|
|
||||||
import {recalculate, updateCombatantDragRulerFlags} from "./socket.js";
|
|
||||||
import {getTokenShape, isClose, zip} from "./util.js";
|
|
||||||
|
|
||||||
function initTrackingFlag(combatant) {
|
|
||||||
const initialFlag = {passedWaypoints: [], trackedRound: 0};
|
|
||||||
let dragRulerFlag = combatant.data.flags.dragRuler;
|
|
||||||
if (dragRulerFlag) {
|
|
||||||
if (isNaN(dragRulerFlag.trackedRound)) {
|
|
||||||
mergeObject(dragRulerFlag, initialFlag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
combatant.data.flags.dragRuler = initialFlag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInitializedCombatant(token, combat) {
|
|
||||||
const combatant = combat.getCombatantByToken(token.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.data.flags.dragRuler.trackedRound) {
|
|
||||||
combatant.data.flags.dragRuler.passedWaypoints = [];
|
|
||||||
combatant.data.flags.dragRuler.trackedRound = combat.data.round;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the passed waypoints to the combatant
|
|
||||||
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active;
|
|
||||||
const dragRulerFlags = combatant.data.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), {terrainRulerInitialState: waypoints[waypoints.length - 1]?.dragRulerFinalState, enableTerrainRuler: terrainRulerAvailable});
|
|
||||||
ray.A.dragRulerVisitedSpaces = ray.terrainRulerVisitedSpaces;
|
|
||||||
ray.A.dragRulerFinalState = 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.id);
|
|
||||||
if (!combatant)
|
|
||||||
return [];
|
|
||||||
const dragRulerFlags = combatant.data.flags.dragRuler;
|
|
||||||
if (!dragRulerFlags)
|
|
||||||
return [];
|
|
||||||
if (combat.data.round > dragRulerFlags.trackedRound)
|
|
||||||
return [];
|
|
||||||
return dragRulerFlags.passedWaypoints ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function removeLastHistoryEntryIfAt(token, x, y) {
|
|
||||||
const history = getMovementHistory(token);
|
|
||||||
if (history.length === 0)
|
|
||||||
return;
|
|
||||||
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)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
history.pop();
|
|
||||||
const combat = game.combat;
|
|
||||||
const combatant = combat.getCombatantByToken(token.id);
|
|
||||||
await updateCombatantDragRulerFlags(combat, [{_id: combatant.id, dragRulerFlags: combatant.data.flags.dragRuler}]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function resetMovementHistory(combat, combatantId) {
|
|
||||||
const combatant = combat.combatants.get(combatantId);
|
|
||||||
const dragRulerFlags = combatant.data.flags.dragRuler;
|
|
||||||
if (!dragRulerFlags)
|
|
||||||
return;
|
|
||||||
dragRulerFlags.passedWaypoints = null;
|
|
||||||
dragRulerFlags.trackedRound = null;
|
|
||||||
dragRulerFlags.rulerState = null;
|
|
||||||
await updateCombatantDragRulerFlags(combat, [{_id: combatantId, dragRulerFlags}]);
|
|
||||||
recalculate();
|
|
||||||
}
|
|
||||||
@@ -1,153 +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 (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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
import {currentSpeedProvider, getColorForDistanceAndToken, getRangesFromSpeedProvider} from "./api.js";
|
|
||||||
import {getHexSizeSupportTokenGridCenter} from "./compatibility.js";
|
|
||||||
import {cancelScheduledMeasurement, measure} from "./foundry_imports.js"
|
|
||||||
import {getMovementHistory} from "./movement_tracking.js";
|
|
||||||
import {wipePathfindingCache} from "./pathfinding.js";
|
|
||||||
import {settingsKey} from "./settings.js";
|
|
||||||
import {getSnapPointForEntity} from "./util.js";
|
|
||||||
|
|
||||||
export function extendRuler() {
|
|
||||||
class DragRulerRuler extends Ruler {
|
|
||||||
// Functions below are overridden versions of functions in Ruler
|
|
||||||
constructor(user, {color=null}={}) {
|
|
||||||
super(user, {color});
|
|
||||||
this.previousWaypoints = [];
|
|
||||||
this.previousLabels = this.addChild(new PIXI.Container());
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
super.clear();
|
|
||||||
this.previousWaypoints = [];
|
|
||||||
this.previousLabels.removeChildren().forEach(c => c.destroy());
|
|
||||||
this.dragRulerRanges = undefined;
|
|
||||||
cancelScheduledMeasurement.call(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
async moveToken(event) {
|
|
||||||
// Disable moveToken if Drag Ruler is active
|
|
||||||
if (!this.isDragRuler)
|
|
||||||
return await super.moveToken(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON() {
|
|
||||||
const json = super.toJSON();
|
|
||||||
if (this.draggedEntity) {
|
|
||||||
const isToken = this.draggedEntity instanceof Token;
|
|
||||||
json.draggedEntityIsToken = isToken;
|
|
||||||
json.draggedEntity = this.draggedEntity.id;
|
|
||||||
json.waypoints = json.waypoints.map(old => {
|
|
||||||
let w = duplicate(old);
|
|
||||||
w.isPathfinding = undefined;
|
|
||||||
return w;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
update(data) {
|
|
||||||
// Don't show a GMs drag ruler to non GM players
|
|
||||||
if (data.draggedEntity && this.user.isGM && !game.user.isGM && !game.settings.get(settingsKey, "showGMRulerToPlayers"))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (data.draggedEntity) {
|
|
||||||
if (data.draggedEntityIsToken)
|
|
||||||
this.draggedEntity = canvas.tokens.get(data.draggedEntity);
|
|
||||||
else
|
|
||||||
this.draggedEntity = canvas.templates.get(data.draggedEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
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.draggedEntity = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The functions below aren't present in the orignal Ruler class and are added by Drag Ruler
|
|
||||||
dragRulerAddWaypoint(point, options={}) {
|
|
||||||
options.snap = options.snap ?? true;
|
|
||||||
if (options.snap) {
|
|
||||||
point = getSnapPointForEntity(point.x, point.y, this.draggedEntity);
|
|
||||||
}
|
|
||||||
this.waypoints.push(new PIXI.Point(point.x, point.y));
|
|
||||||
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
|
|
||||||
this.waypoints.filter(waypoint => waypoint.isPathfinding).forEach(waypoint => waypoint.isPathfinding = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
dragRulerAddWaypointHistory(waypoints) {
|
|
||||||
waypoints.forEach(waypoint => waypoint.isPrevious = true);
|
|
||||||
this.waypoints = this.waypoints.concat(waypoints);
|
|
||||||
for (const waypoint of waypoints) {
|
|
||||||
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dragRulerClearWaypoints() {
|
|
||||||
this.waypoints = [];
|
|
||||||
this.labels.removeChildren().forEach(c => c.destroy());
|
|
||||||
}
|
|
||||||
|
|
||||||
dragRulerDeleteWaypoint(event={preventDefault: () => {return}}, options={}) {
|
|
||||||
this.dragRulerRemovePathfindingWaypoints();
|
|
||||||
options.snap = options.snap ?? true;
|
|
||||||
if (this.waypoints.filter(w => !w.isPrevious).length > 1) {
|
|
||||||
event.preventDefault();
|
|
||||||
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens);
|
|
||||||
const rulerOffset = this.rulerOffset;
|
|
||||||
|
|
||||||
// Options are not passed to _removeWaypoint in vanilla Foundry.
|
|
||||||
// Send them in case other modules have overriden that behavior and accept an options parameter (Toggle Snap to Grid)
|
|
||||||
this._removeWaypoint({x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y}, options);
|
|
||||||
game.user.broadcastActivity({ruler: this});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.dragRulerAbortDrag(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dragRulerRemovePathfindingWaypoints() {
|
|
||||||
this.waypoints.filter(waypoint => waypoint.isPathfinding).forEach(_ => this.labels.removeChild(this.labels.children[this.labels.children.length - 1]).destroy());
|
|
||||||
this.waypoints = this.waypoints.filter(waypoint => !waypoint.isPathfinding);
|
|
||||||
}
|
|
||||||
|
|
||||||
dragRulerAbortDrag(event={preventDefault: () => {return}}) {
|
|
||||||
const token = this.draggedEntity;
|
|
||||||
this._endMeasurement();
|
|
||||||
|
|
||||||
// Deactivate the drag workflow in mouse
|
|
||||||
token.mouseInteractionManager._deactivateDragEvents();
|
|
||||||
token.mouseInteractionManager.state = token.mouseInteractionManager.states.HOVER;
|
|
||||||
|
|
||||||
// This will cancel the current drag operation
|
|
||||||
// Pass in a fake event that hopefully is enough to allow other modules to function
|
|
||||||
token._onDragLeftCancel(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
async dragRulerRecalculate(tokenIds) {
|
|
||||||
if (this._state !== Ruler.STATES.MEASURING)
|
|
||||||
return;
|
|
||||||
if (tokenIds && !tokenIds.includes(this.draggedEntity.id))
|
|
||||||
return;
|
|
||||||
const waypoints = this.waypoints.filter(waypoint => !waypoint.isPrevious);
|
|
||||||
this.dragRulerClearWaypoints();
|
|
||||||
if (game.settings.get(settingsKey, "enableMovementHistory"))
|
|
||||||
this.dragRulerAddWaypointHistory(getMovementHistory(this.draggedEntity));
|
|
||||||
for (const waypoint of waypoints) {
|
|
||||||
this.dragRulerAddWaypoint(waypoint, {snap: false});
|
|
||||||
}
|
|
||||||
this.measure(this.destination);
|
|
||||||
game.user.broadcastActivity({ruler: this});
|
|
||||||
}
|
|
||||||
|
|
||||||
static dragRulerGetRaysFromWaypoints(waypoints, destination) {
|
|
||||||
if ( destination )
|
|
||||||
waypoints = waypoints.concat([destination]);
|
|
||||||
return waypoints.slice(1).map((wp, i) => {
|
|
||||||
const ray = new Ray(waypoints[i], wp);
|
|
||||||
ray.isPrevious = Boolean(waypoints[i].isPrevious);
|
|
||||||
return ray;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
dragRulerGetColorForDistance(distance) {
|
|
||||||
if (!this.isDragRuler)
|
|
||||||
return this.color;
|
|
||||||
if (!this.draggedEntity.actor) {
|
|
||||||
return this.color;
|
|
||||||
}
|
|
||||||
// Don't apply colors if the current user doesn't have at least observer permissions
|
|
||||||
if (this.draggedEntity.actor.permission < 2) {
|
|
||||||
// If this is a pc and alwaysShowSpeedForPCs is enabled we show the color anyway
|
|
||||||
if (!(this.draggedEntity.actor.data.type === "character" && game.settings.get(settingsKey, "alwaysShowSpeedForPCs")))
|
|
||||||
return this.color;
|
|
||||||
}
|
|
||||||
distance = Math.round(distance * 100) / 100;
|
|
||||||
if (!this.dragRulerRanges)
|
|
||||||
this.dragRulerRanges = getRangesFromSpeedProvider(this.draggedEntity);
|
|
||||||
return getColorForDistanceAndToken(distance, this.draggedEntity, this.dragRulerRanges) ?? this.color;
|
|
||||||
}
|
|
||||||
|
|
||||||
dragRulerStart(options, measureImmediately=true) {
|
|
||||||
const entity = this.draggedEntity;
|
|
||||||
const isToken = entity instanceof Token;
|
|
||||||
if (isToken && !currentSpeedProvider.usesRuler(entity))
|
|
||||||
return;
|
|
||||||
const ruler = canvas.controls.ruler;
|
|
||||||
ruler.clear();
|
|
||||||
wipePathfindingCache();
|
|
||||||
ruler._state = Ruler.STATES.STARTING;
|
|
||||||
let entityCenter;
|
|
||||||
if (isToken && canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(entity))
|
|
||||||
entityCenter = getHexSizeSupportTokenGridCenter(entity);
|
|
||||||
else
|
|
||||||
entityCenter = entity.center;
|
|
||||||
if (isToken && game.settings.get(settingsKey, "enableMovementHistory"))
|
|
||||||
ruler.dragRulerAddWaypointHistory(getMovementHistory(entity));
|
|
||||||
ruler.dragRulerAddWaypoint(entityCenter, {snap: false});
|
|
||||||
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens);
|
|
||||||
const destination = {x: mousePosition.x + ruler.rulerOffset.x, y: mousePosition.y + ruler.rulerOffset.y};
|
|
||||||
if (measureImmediately)
|
|
||||||
ruler.measure(destination, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ruler = DragRulerRuler;
|
|
||||||
}
|
|
||||||
@@ -1,305 +1,38 @@
|
|||||||
import {availableSpeedProviders, currentSpeedProvider, getDefaultSpeedProvider, updateSpeedProvider} from "./api.js";
|
import {updateSpeedProvider} from "./api.js";
|
||||||
import {SpeedProvider} from "./speed_provider.js"
|
import {getDefaultDashMultiplier, getDefaultSpeedAttribute} from "./systems.js"
|
||||||
import { early_isGM } from "./util.js";
|
|
||||||
|
|
||||||
export const settingsKey = "drag-ruler";
|
export const settingsKey = "drag-ruler";
|
||||||
|
|
||||||
export const RightClickAction = Object.freeze({
|
|
||||||
CREATE_WAYPOINT: 0,
|
|
||||||
DELETE_WAYPOINT: 1,
|
|
||||||
ABORT_DRAG:2,
|
|
||||||
});
|
|
||||||
|
|
||||||
export function registerSettings() {
|
export function registerSettings() {
|
||||||
game.settings.register(settingsKey, "dataVersion", {
|
|
||||||
scope: "world",
|
|
||||||
config: false,
|
|
||||||
type: String,
|
|
||||||
default: "fresh install"
|
|
||||||
})
|
|
||||||
|
|
||||||
game.settings.register(settingsKey, "clientDataVersion", {
|
|
||||||
scope: "client",
|
|
||||||
config: false,
|
|
||||||
type: String,
|
|
||||||
default: "fresh install"
|
|
||||||
});
|
|
||||||
|
|
||||||
game.settings.register(settingsKey, "rightClickAction", {
|
|
||||||
name: "drag-ruler.settings.rightClickAction.name",
|
|
||||||
hint: "drag-ruler.settings.rightClickAction.hint",
|
|
||||||
config: true,
|
|
||||||
type: Number,
|
|
||||||
default: RightClickAction.DELETE_WAYPOINT,
|
|
||||||
choices: {
|
|
||||||
0: "drag-ruler.settings.rightClickAction.choices.create",
|
|
||||||
1: "drag-ruler.settings.rightClickAction.choices.delete",
|
|
||||||
2: "drag-ruler.settings.rightClickAction.choices.cancel",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
game.settings.register(settingsKey, "autoStartMeasurement", {
|
|
||||||
name: "drag-ruler.settings.autoStartMeasurement.name",
|
|
||||||
hint: "drag-ruler.settings.autoStartMeasurement.hint",
|
|
||||||
scope: "client",
|
|
||||||
config: true,
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
game.settings.register(settingsKey, "useGridlessRaster", {
|
|
||||||
name: "drag-ruler.settings.useGridlessRaster.name",
|
|
||||||
hint: "drag-ruler.settings.useGridlessRaster.hint",
|
|
||||||
scope: "client",
|
|
||||||
config: true,
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
game.settings.register(settingsKey, "alwaysShowSpeedForPCs", {
|
|
||||||
name: "drag-ruler.settings.alwaysShowSpeedForPCs.name",
|
|
||||||
hint: "drag-ruler.settings.alwaysShowSpeedForPCs.hint",
|
|
||||||
scope: "world",
|
|
||||||
config: true,
|
|
||||||
type: Boolean,
|
|
||||||
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, "allowPathfinding", {
|
|
||||||
name: "drag-ruler.settings.allowPathfinding.name",
|
|
||||||
hint: "drag-ruler.settings.allowPathfinding.hint",
|
|
||||||
scope: "world",
|
|
||||||
config: true,
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
onChange: () => location.reload(),
|
|
||||||
});
|
|
||||||
|
|
||||||
game.settings.register(settingsKey, "autoPathfinding", {
|
|
||||||
name: "drag-ruler.settings.autoPathfinding.name",
|
|
||||||
hint: "drag-ruler.settings.autoPathfinding.hint",
|
|
||||||
scpoe: "client",
|
|
||||||
config: early_isGM(),
|
|
||||||
type: Boolean,
|
|
||||||
defualt: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
// This setting will be modified by the api if modules register to it
|
||||||
game.settings.register(settingsKey, "speedProvider", {
|
game.settings.register(settingsKey, "speedProvider", {
|
||||||
|
name: "drag-ruler.settings.speedProvider.name",
|
||||||
|
hint: "drag-ruler.settings.speedProvider.hint",
|
||||||
scope: "world",
|
scope: "world",
|
||||||
config: false,
|
config: false,
|
||||||
type: String,
|
type: Object,
|
||||||
default: getDefaultSpeedProvider(),
|
choices: {
|
||||||
|
"native": game.i18n.localize("drag-ruler.settings.speedProvider.choices.native")
|
||||||
|
},
|
||||||
|
default: "native",
|
||||||
onChange: updateSpeedProvider,
|
onChange: updateSpeedProvider,
|
||||||
})
|
})
|
||||||
|
|
||||||
game.settings.registerMenu(settingsKey, "speedProviderSettings", {
|
game.settings.register(settingsKey, "speedAttribute", {
|
||||||
name: "drag-ruler.settings.speedProviderSettings.name",
|
name: "drag-ruler.settings.speedAttribute.name",
|
||||||
hint: "drag-ruler.settings.speedProviderSettings.hint",
|
hint: "drag-ruler.settings.speedAttribute.hint",
|
||||||
label: "drag-ruler.settings.speedProviderSettings.button",
|
scope: "world",
|
||||||
icon: "fas fa-tachometer-alt",
|
config: true,
|
||||||
type: SpeedProviderSettings,
|
|
||||||
restricted: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
class SpeedProviderSettings extends FormApplication {
|
|
||||||
static get defaultOptions() {
|
|
||||||
return mergeObject(super.defaultOptions, {
|
|
||||||
id: "drag-ruler-speed-provider-settings",
|
|
||||||
title: game.i18n.localize("drag-ruler.settings.speedProviderSettings.windowTitle"),
|
|
||||||
template: "modules/drag-ruler/templates/speed_provider_settings.html",
|
|
||||||
width: 600,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getData(options={}) {
|
|
||||||
const data = {}
|
|
||||||
data.isGM = game.user.isGM
|
|
||||||
const selectedProvider = currentSpeedProvider.id
|
|
||||||
|
|
||||||
// Insert all speed providers into the template data
|
|
||||||
data.providers = Object.values(availableSpeedProviders).map(speedProvider => {
|
|
||||||
const provider = {}
|
|
||||||
provider.id = speedProvider.id
|
|
||||||
provider.hasSettings = speedProvider instanceof SpeedProvider
|
|
||||||
if (provider.hasSettings)
|
|
||||||
provider.settings = enumerateProviderSettings(speedProvider)
|
|
||||||
let dotPosition = provider.id.indexOf(".")
|
|
||||||
if (dotPosition === -1)
|
|
||||||
dotPosition = provider.id.length
|
|
||||||
const type = provider.id.substring(0, dotPosition)
|
|
||||||
const id = provider.id.substring(dotPosition + 1)
|
|
||||||
if (type === "native") {
|
|
||||||
provider.selectTitle = game.i18n.localize("drag-ruler.settings.speedProviderSettings.speedProvider.choices.native")
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let name
|
|
||||||
if (type === "module") {
|
|
||||||
name = game.modules.get(id).data.title
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
name = game.system.data.title
|
|
||||||
}
|
|
||||||
provider.selectTitle = game.i18n.format(`drag-ruler.settings.speedProviderSettings.speedProvider.choices.${type}`, {name})
|
|
||||||
}
|
|
||||||
provider.isSelected = provider.id === selectedProvider
|
|
||||||
return provider
|
|
||||||
})
|
|
||||||
data.selectedProviderName = data.providers.find(provider => provider.isSelected).selectTitle
|
|
||||||
|
|
||||||
data.providerSelection = {
|
|
||||||
id: "speedProvider",
|
|
||||||
name: game.i18n.localize("drag-ruler.settings.speedProviderSettings.speedProvider.name"),
|
|
||||||
hint: game.i18n.localize("drag-ruler.settings.speedProviderSettings.speedProvider.hint"),
|
|
||||||
type: String,
|
type: String,
|
||||||
choices: data.providers.reduce((choices, provider) => {
|
default: getDefaultSpeedAttribute(),
|
||||||
choices[provider.id] = provider.selectTitle
|
|
||||||
return choices
|
|
||||||
}, {}),
|
|
||||||
value: selectedProvider,
|
|
||||||
isCheckbox: false,
|
|
||||||
isSelect: true,
|
|
||||||
isRange: false,
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
async _updateObject(event, formData) {
|
|
||||||
const selectedSpeedProvider = game.user.isGM ? formData.speedProvider : game.settings.get(settingsKey, "speedProvider")
|
|
||||||
for (let [key, value] of Object.entries(formData)) {
|
|
||||||
// Check if this is color, convert the value to an integer
|
|
||||||
const splitKey = key.split(".", 3)
|
|
||||||
if (splitKey[0] !== "native")
|
|
||||||
splitKey.shift()
|
|
||||||
if (splitKey.length >= 2 && splitKey[1] == "color") {
|
|
||||||
value = parseInt(value.substring(1), 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't change settings for speed providers that aren't currently active
|
|
||||||
if (key !== "speedProvider" && !key.startsWith(selectedSpeedProvider))
|
|
||||||
continue
|
|
||||||
|
|
||||||
// Get the key for the current setting
|
|
||||||
let setting
|
|
||||||
if (key === "speedProvider")
|
|
||||||
setting = "speedProvider"
|
|
||||||
else
|
|
||||||
setting = `speedProviders.${key}`
|
|
||||||
|
|
||||||
// Get the old setting value
|
|
||||||
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)
|
|
||||||
if (value !== oldValue)
|
|
||||||
game.settings.set(settingsKey, setting, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activate the configured speed provider
|
|
||||||
updateSpeedProvider()
|
|
||||||
}
|
|
||||||
|
|
||||||
activateListeners(html) {
|
|
||||||
super.activateListeners(html)
|
|
||||||
html.find("select[name=speedProvider]").change(this.onSpeedProviderChange.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
onSpeedProviderChange(event) {
|
|
||||||
// Hide all module settings
|
|
||||||
document.querySelectorAll(".drag-ruler-provider-settings").forEach(element => element.style.display = "none")
|
|
||||||
// Show the settings block for the currently selected module
|
|
||||||
document.getElementById(`drag-ruler.provider.${event.currentTarget.value}`).style.display = ""
|
|
||||||
|
|
||||||
// Recalculate window height
|
|
||||||
this.element[0].style.height = null
|
|
||||||
this.position.height = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toDomHex(value) {
|
|
||||||
const hex = value.toString(16)
|
|
||||||
return "#" + "0".repeat(Math.max(0, 6 - hex.length)) + hex
|
|
||||||
}
|
|
||||||
|
|
||||||
function enumerateProviderSettings(provider) {
|
|
||||||
const colorSettings = []
|
|
||||||
const unreachableColor = {id: "unreachable", name: "drag-ruler.settings.speedProviderSettings.color.unreachable.name"}
|
|
||||||
|
|
||||||
// Resolve settings for the colors
|
|
||||||
for (const color of provider.colors.concat([unreachableColor])) {
|
|
||||||
// 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
|
|
||||||
let hint
|
|
||||||
if (color === unreachableColor)
|
|
||||||
hint = game.i18n.localize("drag-ruler.settings.speedProviderSettings.color.unreachable.hint")
|
|
||||||
else
|
|
||||||
hint = game.i18n.format("drag-ruler.settings.speedProviderSettings.color.hint", {colorName})
|
|
||||||
colorSettings.push({
|
|
||||||
id: `${provider.id}.color.${color.id}`,
|
|
||||||
name: game.i18n.format("drag-ruler.settings.speedProviderSettings.color.name", {colorName}),
|
|
||||||
hint: hint,
|
|
||||||
type: Number,
|
|
||||||
value: toDomHex(game.settings.get(settingsKey, `speedProviders.${provider.id}.color.${color.id}`)),
|
|
||||||
isCheckbox: false,
|
|
||||||
isSelect: false,
|
|
||||||
isRange: false,
|
|
||||||
isColor: true,
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare regular settings
|
game.settings.register(settingsKey, "dashMultiplier", {
|
||||||
const settings = []
|
name: "drag-ruler.settings.dashMultiplier.name",
|
||||||
for (const setting of provider.settings) {
|
hint: "drag-ruler.settings.dashMultiplier.hint",
|
||||||
try {
|
scope: "world",
|
||||||
if (setting.scope === "world" && !game.user.isGM)
|
config: true,
|
||||||
continue
|
type: Number,
|
||||||
const s = duplicate(setting)
|
default: getDefaultDashMultiplier(),
|
||||||
s.id = `${provider.id}.setting.${s.id}`
|
})
|
||||||
s.name = game.i18n.localize(s.name)
|
|
||||||
s.hint = game.i18n.localize(s.hint)
|
|
||||||
s.value = provider.getSetting(setting.id)
|
|
||||||
s.type = setting.type instanceof Function ? setting.type.name : "String"
|
|
||||||
s.isCheckbox = setting.type === Boolean
|
|
||||||
s.isSelect = s.choices !== undefined
|
|
||||||
s.isRange = (setting.type === Number) && s.range
|
|
||||||
s.isColor = false
|
|
||||||
settings.push(s)
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.warn(`Drag Ruler | The following error occured while rendering setting "${setting.id}" of module/system "${this.id}. It won't be displayed.`)
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings.concat(colorSettings)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
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.combatants.get(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.combatants.get(update._id).actor;
|
|
||||||
if (!actor)
|
|
||||||
return false;
|
|
||||||
return actor.testUserPermission(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.updateEmbeddedDocuments("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);
|
|
||||||
}
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
import {settingsKey} from "./settings.js"
|
|
||||||
import {getDefaultDashMultiplier, getDefaultSpeedAttribute} from "./systems.js"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base class for all speed providers.
|
|
||||||
* If you want to offer a speed provider in your system/module you must derive this class.
|
|
||||||
* Each speed provider must at least implement
|
|
||||||
*/
|
|
||||||
export class SpeedProvider {
|
|
||||||
/**
|
|
||||||
* Returns an array of colors used by this speed provider. Each color corresponds to one speed that a token may have.
|
|
||||||
* Each color must be an object with the following properties:
|
|
||||||
* - id: A value that identfies the color. Must be unique for each color returned.
|
|
||||||
* - default: The color that is used to highlight that speed by default.
|
|
||||||
* - name: A user readable name for the speed represented by the color. This name is used in the color configuration dialog. Drag Ruler will attempt to localize this string using `game.i18n`
|
|
||||||
*
|
|
||||||
* Of these properties, id and defaultColor are required. name is optional, but it's recommended to set it
|
|
||||||
*
|
|
||||||
* Implementing this method is required for all speed providers
|
|
||||||
*/
|
|
||||||
get colors() {
|
|
||||||
throw new Error("A SpeedProvider must implement the colors function")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an array of speeds that the token passed in the arguments this token can reach.
|
|
||||||
* Each range is an object that with the following properties:
|
|
||||||
* - range: A number indicating the distance that the token can travel with this speed
|
|
||||||
* - color: The id (as defined in the `colors` getter) of the color that should be used to represent this range
|
|
||||||
*
|
|
||||||
* Implementing this method is required for all speed providers
|
|
||||||
*/
|
|
||||||
getRanges(token) {
|
|
||||||
throw new Error("A SpeedProvider must implement the getRanges function")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an array of configuration options for this module. The settings will be shown in the Speed Provider Settings of Drag Ruler.
|
|
||||||
* Each configuration option is an object that has the same attributes as a native foundry setting passed to `game.settings.register`,
|
|
||||||
* except for these exceptions:
|
|
||||||
* - id: A string that identifies the setting. Must be unique for each setting returned. This id will be used to fetch the setting.
|
|
||||||
* - config: This property is not supported by Drag Ruler module settings. Use foundries native settings instead if you need settings that don't show up in the configuration dialog.
|
|
||||||
*
|
|
||||||
* Implementing this method is optional and only needs to be done if you want to provide custom provider settings
|
|
||||||
*/
|
|
||||||
get settings() {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the default color for ranges that a token cannot reach.
|
|
||||||
*
|
|
||||||
* Implementing this method is optional and only needs to be done if you want to provide a custom default for that color.
|
|
||||||
*/
|
|
||||||
get defaultUnreachableColor() {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
* returns false will behave as if Drag Ruler wasn't installed.
|
|
||||||
* If usesRuler returns `false` it's guranteed that the `getRanges` function won't be called for that token.
|
|
||||||
*
|
|
||||||
* Implementing this method is optional and only needs to be done if you want to disable Drag Ruler for some tokens.
|
|
||||||
*/
|
|
||||||
usesRuler(token) {
|
|
||||||
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.
|
|
||||||
*
|
|
||||||
* This function shouldn't be overridden by speed provider implementations. It can be called to fetch speed provider specific settings.
|
|
||||||
*/
|
|
||||||
getSetting(settingId) {
|
|
||||||
try {
|
|
||||||
return game.settings.get(settingsKey, `speedProviders.${this.id}.setting.${settingId}`)
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
if (this.settings.some(setting => setting.id === settingId)) {
|
|
||||||
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.`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new instance of he speed provider
|
|
||||||
*
|
|
||||||
* This function should neither be called or overridden by speed provider implementations
|
|
||||||
*/
|
|
||||||
constructor(id) {
|
|
||||||
this.id = id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class GenericSpeedProvider extends SpeedProvider {
|
|
||||||
get colors() {
|
|
||||||
return [
|
|
||||||
{id: "walk", default: 0x00FF00, name: "drag-ruler.genericSpeedProvider.speeds.walk"},
|
|
||||||
{id: "dash", default: 0xFFFF00, name: "drag-ruler.genericSpeedProvider.speeds.dash"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
getRanges(token) {
|
|
||||||
const speedAttribute = this.getSetting("speedAttribute")
|
|
||||||
if (!speedAttribute)
|
|
||||||
return []
|
|
||||||
const tokenSpeed = parseFloat(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 []
|
|
||||||
}
|
|
||||||
const dashMultiplier = this.getSetting("dashMultiplier")
|
|
||||||
if (!dashMultiplier)
|
|
||||||
return [{range: tokenSpeed, color: "walk"}]
|
|
||||||
return [{range: tokenSpeed, color: "walk"}, {range: tokenSpeed * dashMultiplier, color: "dash"}]
|
|
||||||
}
|
|
||||||
|
|
||||||
get settings() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: "speedAttribute",
|
|
||||||
name: "drag-ruler.genericSpeedProvider.settings.speedAttribute.name",
|
|
||||||
hint: "drag-ruler.genericSpeedProvider.settings.speedAttribute.hint",
|
|
||||||
scope: "world",
|
|
||||||
config: true,
|
|
||||||
type: String,
|
|
||||||
default: getDefaultSpeedAttribute(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "dashMultiplier",
|
|
||||||
name: "drag-ruler.genericSpeedProvider.settings.dashMultiplier.name",
|
|
||||||
hint: "drag-ruler.genericSpeedProvider.settings.dashMultiplier.hint",
|
|
||||||
scope: "world",
|
|
||||||
config: true,
|
|
||||||
type: Number,
|
|
||||||
default: getDefaultDashMultiplier(),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +1,19 @@
|
|||||||
|
|
||||||
export function getDefaultSpeedAttribute() {
|
export function getDefaultSpeedAttribute() {
|
||||||
switch (game.system.id) {
|
switch (game.system.id) {
|
||||||
case "CoC7":
|
|
||||||
return "actor.data.data.attribs.mov.value";
|
|
||||||
case "dcc":
|
|
||||||
return "actor.data.data.attributes.speed.value";
|
|
||||||
case "dnd5e":
|
case "dnd5e":
|
||||||
return "actor.data.data.attributes.movement.walk"
|
return "actor.data.data.attributes.movement.walk"
|
||||||
case "lancer":
|
|
||||||
return "actor.data.data.derived.speed"
|
|
||||||
case "pf1":
|
case "pf1":
|
||||||
case "D35E":
|
|
||||||
return "actor.data.data.attributes.speed.land.total"
|
return "actor.data.data.attributes.speed.land.total"
|
||||||
case "sfrpg":
|
|
||||||
return "actor.data.data.attributes.speed.value";
|
|
||||||
case "shadowrun5e":
|
|
||||||
return "actor.data.data.movement.walk.value";
|
|
||||||
case "swade":
|
|
||||||
return "actor.data.data.stats.speed.adjusted";
|
|
||||||
case "ds4":
|
|
||||||
return "actor.data.data.combatValues.movement.total";
|
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaultDashMultiplier() {
|
export function getDefaultDashMultiplier() {
|
||||||
switch (game.system.id) {
|
switch (game.system.id) {
|
||||||
case "swade":
|
|
||||||
return 0
|
|
||||||
case "dcc":
|
|
||||||
case "dnd5e":
|
case "dnd5e":
|
||||||
case "lancer":
|
|
||||||
case "pf1":
|
case "pf1":
|
||||||
case "D35E":
|
|
||||||
case "sfrpg":
|
|
||||||
case "shadowrun5e":
|
|
||||||
case "ds4":
|
|
||||||
return 2
|
return 2
|
||||||
case "CoC7":
|
|
||||||
return 5;
|
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,281 +0,0 @@
|
|||||||
import {getPixelsFromGridPosition} from "./foundry_fixes.js"
|
|
||||||
import { disableSnap } from "./keybindings.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* enumeratedZip(it1, it2) {
|
|
||||||
let i = 0;
|
|
||||||
for (const [v1, v2] of zip(it1, it2)) {
|
|
||||||
yield [i, v1, v2];
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function* iterPairs(l) {
|
|
||||||
for (let i = 1;i < l.length;i++) {
|
|
||||||
yield [l[i - 1], l[i]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sum(arr) {
|
|
||||||
return arr.reduce((a, b) => a + b, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSnapPointForToken(x, y, token) {
|
|
||||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
|
||||||
return new PIXI.Point(x, y);
|
|
||||||
}
|
|
||||||
if (canvas.grid.isHex) {
|
|
||||||
if (game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) {
|
|
||||||
if (token.document.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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return new PIXI.Point(...canvas.grid.getCenter(x, y));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [topLeftX, topLeftY] = canvas.grid.getTopLeft(x, y);
|
|
||||||
let cellX, cellY;
|
|
||||||
if (token.data.width % 2 === 0)
|
|
||||||
cellX = x - canvas.grid.h / 2;
|
|
||||||
else
|
|
||||||
cellX = x;
|
|
||||||
if (token.data.height % 2 === 0)
|
|
||||||
cellY = y - canvas.grid.h / 2;
|
|
||||||
else
|
|
||||||
cellY = y;
|
|
||||||
const [centerX, centerY] = canvas.grid.getCenter(cellX, cellY);
|
|
||||||
let snapX, snapY;
|
|
||||||
// Tiny tokens can snap to the cells corners
|
|
||||||
if (token.data.width <= 0.5) {
|
|
||||||
const offsetX = x - topLeftX;
|
|
||||||
const subGridWidth = Math.floor(canvas.grid.w / 2);
|
|
||||||
const subGridPosX = Math.floor(offsetX / 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
|
|
||||||
else if (Math.round(token.data.width) % 2 === 1 || token.data.width < 1) {
|
|
||||||
snapX = centerX;
|
|
||||||
}
|
|
||||||
// All remaining tokens (those with even or fractional multipliers on square grids) snap to the intersection points of the grid
|
|
||||||
else {
|
|
||||||
snapX = centerX + canvas.grid.w / 2;
|
|
||||||
}
|
|
||||||
if (token.data.height <= 0.5) {
|
|
||||||
const offsetY = y - topLeftY;
|
|
||||||
const subGridHeight = Math.floor(canvas.grid.h / 2);
|
|
||||||
const subGridPosY = Math.floor(offsetY / subGridHeight);
|
|
||||||
snapY = topLeftY + (subGridPosY + 0.5) * subGridHeight;
|
|
||||||
}
|
|
||||||
else if (Math.round(token.data.height) % 2 === 1 || token.data.height < 1) {
|
|
||||||
snapY = centerY;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
snapY = centerY + canvas.grid.h / 2;
|
|
||||||
}
|
|
||||||
return new PIXI.Point(snapX, snapY);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSnapPointForTokenObj(pos, token) {
|
|
||||||
return getSnapPointForToken(pos.x, pos.y, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSnapPointForMeasuredTemplate(x, y) {
|
|
||||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
|
||||||
return new PIXI.Point(x, y);
|
|
||||||
}
|
|
||||||
let subgridWidth, subgridHeight;
|
|
||||||
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) {
|
|
||||||
const isToken = entity instanceof Token;
|
|
||||||
if (isToken)
|
|
||||||
return getSnapPointForToken(x, y, entity);
|
|
||||||
else
|
|
||||||
return getSnapPointForMeasuredTemplate(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setSnapParameterOnOptions(sourceObject, options) {
|
|
||||||
// Allow outside modules to override snapping
|
|
||||||
if (sourceObject.snapOverride?.active) {
|
|
||||||
options.snapOverrideActive = true;
|
|
||||||
options.snap = sourceObject.snapOverride.snap;
|
|
||||||
sourceObject.snapOverride = undefined; // remove it to prevent any lingering data issues
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
options.snap = !disableSnap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isClose(a, b, delta) {
|
|
||||||
return Math.abs(a - b) <= delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMeasurePosition() {
|
|
||||||
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens);
|
|
||||||
const rulerOffset = canvas.controls.ruler.rulerOffset;
|
|
||||||
const measurePosition = {x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y};
|
|
||||||
return measurePosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
// isGM function for use during loading when game.user isn't available yet
|
|
||||||
export function early_isGM() {
|
|
||||||
const level = game.data.users.find(u => u._id == game.data.userId).role;
|
|
||||||
const gmLevel = CONST.USER_ROLES.ASSISTANT;
|
|
||||||
return level >= gmLevel;
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
{{! This partial is based on the foundry settings partial}}
|
|
||||||
{{#*inline "settingPartial"}}
|
|
||||||
<div class="form-group">
|
|
||||||
<label>{{this.name}}</label>
|
|
||||||
<div class="form-fields">
|
|
||||||
{{#if this.isCheckbox}}
|
|
||||||
<input type="checkbox" name="{{this.id}}" data-dtype="Boolean" {{checked this.value}} />
|
|
||||||
{{else if this.isSelect}}
|
|
||||||
<select name="{{this.id}}">
|
|
||||||
{{#select this.value}}
|
|
||||||
{{#each this.choices as |name k|}}
|
|
||||||
<option value="{{k}}">{{localize name}}</option>
|
|
||||||
{{/each}}
|
|
||||||
{{/select}}
|
|
||||||
</select>
|
|
||||||
{{else if this.isRange}}
|
|
||||||
<input type="range" name="{{this.id}}" data-dtype="Number" value="{{ this.value }}"
|
|
||||||
min="{{ this.range.min }}" max="{{ this.range.max }}" step="{{ this.range.step }}" />
|
|
||||||
<span class="range-value">{{this.value}}</span>
|
|
||||||
{{else if this.isColor}}
|
|
||||||
<input type="color" name="{{this.id}}" value="{{this.value}}" data-dtype="{{this.type}}" />
|
|
||||||
{{else}}
|
|
||||||
<input type="text" name="{{this.id}}" value="{{this.value}}" data-dtype="{{this.type}}" />
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
<p class="notes">{{this.hint}}</p>
|
|
||||||
</div>
|
|
||||||
{{/inline}}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<form class="flexcol" autocomplete="off">
|
|
||||||
<section class="content">
|
|
||||||
<div class="settings-list">
|
|
||||||
<h2 class="module-header">{{localize "drag-ruler.settings.speedProviderSettings.headers.speedProvider"}}</h2>
|
|
||||||
{{#if this.isGM}}
|
|
||||||
{{#with this.providerSelection}}
|
|
||||||
{{> settingPartial}}
|
|
||||||
{{/with}}
|
|
||||||
{{else}}
|
|
||||||
<div class="form-group"><label>{{localize "drag-ruler.settings.speedProviderSettings.activeProvider.name"}}</label><div class="form-fields" style="justify-content: flex-start;"><b>{{this.selectedProviderName}}</b></div></div>
|
|
||||||
<p class="notes">{{localize "drag-ruler.settings.speedProviderSettings.activeProvider.hint"}}</p>
|
|
||||||
{{/if}}
|
|
||||||
<h2 class="module-header">{{localize "drag-ruler.settings.speedProviderSettings.headers.speedProviderSettings"}}</h2>
|
|
||||||
{{#each this.providers}}
|
|
||||||
<div class="drag-ruler-provider-settings" id="drag-ruler.provider.{{this.id}}" {{#unless this.isSelected}}style="display:none"{{/unless}}>
|
|
||||||
{{#if this.hasSettings}}
|
|
||||||
{{#each settings}}
|
|
||||||
{{> settingPartial}}
|
|
||||||
{{/each}}
|
|
||||||
{{else}}
|
|
||||||
<p>{{localize "drag-ruler.settings.speedProviderSettings.noSettings"}}</p>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<footer class="sheet-footer flexrow">
|
|
||||||
<button type="submit" name="submit">
|
|
||||||
<i class="far fa-save"></i> {{localize 'SETTINGS.Save'}}
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
</form>
|
|
||||||