Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bed16de575 | |||
| 891bb1b4d8 | |||
| 43f26088b5 | |||
| 39a0787c79 | |||
| 3be898e49c | |||
| 817662bf30 | |||
| 56527ccf75 | |||
| 6bf8083f7a | |||
| b5ea1f2284 | |||
| 6d9870bedb | |||
| afbe5f9473 | |||
| bae0e43b7b | |||
| 1acc012d65 | |||
| 4671e6de51 | |||
| f0ef109658 | |||
| f0d1ef9d48 | |||
| d04ea9b0b7 | |||
| ba8ab9d473 | |||
| 2691720090 | |||
| 70b166d844 | |||
| 908600bfa3 | |||
| 6472c1d5bd | |||
| 9151b45874 | |||
| d732f7ca17 | |||
| 4be6730fd5 | |||
| 1faeda6f47 | |||
| 667259d5d6 | |||
| 4b68b82590 | |||
| 447995977b | |||
| 9e5b344b0d | |||
| 43c5df42d4 | |||
| d0ee47d551 | |||
| 27ffb2d8cb | |||
| b734257515 | |||
| ff5d46ebbe | |||
| 8f896d9b82 | |||
| f7683e8945 | |||
| 9787a41fd1 | |||
| 5d93c61f0c | |||
| 00e8a9781e | |||
| d2f2307e4d | |||
| 65912df989 | |||
| 9ae9dae427 | |||
| 8cd5d7f276 | |||
| 35624a37aa | |||
| c66ec46aed | |||
| dec422fa3a | |||
| 6ad1f4c522 | |||
| fb9a2ee60c | |||
| 57ed545e30 | |||
| 94fe223529 | |||
| 305a6116bc | |||
| 942bfa04e8 | |||
| b4ac65e478 | |||
| 0de01379e9 | |||
| f6eb5eff9c | |||
| fee0983841 | |||
| 3de044ea45 | |||
| e473fea121 | |||
| 0aee541eb3 | |||
| 40dde08ddd | |||
| 8234aa566d | |||
| 2862643ba8 | |||
| e3abdeab3a | |||
| 2ff4e98fe9 | |||
| e0d0483722 | |||
| 4f1dec3089 | |||
| c4d089e8ff | |||
| f8fe4ee971 | |||
| 378401b5bb | |||
| 0a54a47951 | |||
| 0b71504da3 | |||
| 5a7f5531f2 | |||
| 862477c698 | |||
| 384de1a1b5 | |||
| 0a7d8495c6 | |||
| 6e74582089 | |||
| 20e8713e30 | |||
| 544740b697 | |||
| 404e978377 | |||
| 4a56cdb0a4 | |||
| 48e6901b59 | |||
| 15e910424f | |||
| baba6e2986 | |||
| 6f0a04c20f | |||
| 8522c9a811 | |||
| d5fd5e0d04 | |||
| aabe9c75e8 | |||
| 0fb8aecfdd | |||
| bb5fe94439 | |||
| 6f6655009d | |||
| 8110a1b78b | |||
| b84cdfd7c1 | |||
| 75d59171d6 | |||
| 6ea03579b9 | |||
| fd5b439ec2 | |||
| 52118a6142 | |||
| b29a687fff |
+145
@@ -1,3 +1,148 @@
|
|||||||
|
## 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
|
## 1.2.2
|
||||||
### Translation
|
### Translation
|
||||||
- Added japanese translation (thanks to touge)
|
- Added japanese translation (thanks to touge)
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ You can add waypoints to the path by pressing spacebar while you drag the token.
|
|||||||
|
|
||||||

|

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

|
||||||
|
|
||||||
|
|
||||||
## Why would I want to use this instead of ShowDragDistance?
|
## Why would I want to use this instead of ShowDragDistance?
|
||||||
ShowDragDistance isn't maintained anymore. This means that it is at risk to stop working with every foundry update. In fact this process has already begun. As of Foundry Version 0.7.9 ShowDragDistance doesn't work anymore on gridless maps. Drag Ruler on the other hand is fully compatible with the current Foundry release and I'll continue updating it for future foundry releases for the forseeable future.
|
ShowDragDistance isn't maintained anymore. This means that it is at risk to stop working with every foundry update. In fact this process has already begun. As of Foundry Version 0.7.9 ShowDragDistance doesn't work anymore on gridless maps. Drag Ruler on the other hand is fully compatible with the current Foundry release and I'll continue updating it for future foundry releases for the forseeable future.
|
||||||
@@ -26,67 +33,112 @@ In addition Drag Ruler provides more flexibility for game systems and modules vi
|
|||||||
Drag Ruler will work with all Foundry VTT game systems. However some game systems offer a special integration via the [Drag Ruler API](#api), that allows Drag Ruler to take the rules of the game system into account when dispaying speeds (such as weight carried or conditions that apply to the character), offering a smoother experience. While some game systems offer this integration natively, for other game systems there are modules providing the integration. If the integration is provided via a module you need to install and activate both Drag Ruler and the integration module to benefit from the integration.
|
Drag Ruler will work with all Foundry VTT game systems. However some game systems offer a special integration via the [Drag Ruler API](#api), that allows Drag Ruler to take the rules of the game system into account when dispaying speeds (such as weight carried or conditions that apply to the character), offering a smoother experience. While some game systems offer this integration natively, for other game systems there are modules providing the integration. If the integration is provided via a module you need to install and activate both Drag Ruler and the integration module to benefit from the integration.
|
||||||
|
|
||||||
The game systems that offer Drag Ruler integration are:
|
The game systems that offer Drag Ruler integration are:
|
||||||
|
- Cypher System (starting with version 1.13.0)
|
||||||
- Pathfinder 1 (starting with version 0.77.3)
|
- Pathfinder 1 (starting with version 0.77.3)
|
||||||
- Pathfinder 2e (via the module [PF2E Drag Ruler Integration](https://foundryvtt.com/packages/pf2e-dragruler/))
|
- Pathfinder 2e (via the module [PF2E Drag Ruler Integration](https://foundryvtt.com/packages/pf2e-dragruler/))
|
||||||
|
- Tormenta20 (starting with version 1.1.37)
|
||||||
|
|
||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
Drag Ruler is available in the follwing languages:
|
Drag Ruler is available in the follwing languages:
|
||||||
|
- Chinese (thanks to zeteticl)
|
||||||
|
- English
|
||||||
|
- German
|
||||||
- Japanese (thanks to touge)
|
- Japanese (thanks to touge)
|
||||||
|
|
||||||
## API
|
## 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.*
|
*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 gives a short introduction on how to accomplish this, followed by a full example of how a call to the api might look like.
|
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.
|
||||||
|
|
||||||
### Registering your Module/System
|
|
||||||
*This section is written from the perspective of a module developer. If you're writing a game system the process is exactly the same, except that you need to invoke `registerSystem` instead of `registerModule` and need to use the id of your system instead of a module id.*
|
|
||||||
|
|
||||||
The first thing you'll have to do is to make your module known to Drag Ruler. To do this, you have to call the `dragRuler.registerModule` function after the `dragRuler.ready` hook has fired. This might look like this:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
Hooks.once("dragRuler.ready", () => {
|
|
||||||
dragRuler.registerModule("id-of-your-module", mySpeedProvider)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
The function `registerModule` takes two parameters: The first parameter must be the ID of your module as specified in your manifest. The second parameter is a reference to the Speed Provider of your module. What that Speed Provider looks like will be detailed in the paragraph [Writing the Speed Provider](#writing-the-speed-provider)
|
|
||||||
|
|
||||||
### Writing the Speed Provider
|
|
||||||
The Speed Provider is a function that calculates the speeds that a token can reach and assigns a color to each of the speeds. That function receives two arguments. The first argument is the token for which the speed should be calculated. The second argument is the color of the player that is dragging the token.
|
|
||||||
|
|
||||||
The function should contain an array of objects in the format `{range: Integer, color: Integer}`. The range indicates a distance that the token can cover with a certain speed and the color indicates which color should be used for that range. Spaces that cannot be reached with any of the speeds provided by this function will be colored in red (`0xFF0000`). The returned array doesn't need to be sorted in any particular order. However the array is not allowe to contain two objects with an identical range, as that results in undefined behavior.
|
|
||||||
|
|
||||||
The Speed Provider will be called once for each square that will be colored. For this reason it is advisable to not perform any expensive calculations in the Speed Provider.
|
|
||||||
|
|
||||||
Here is an example of how a Speed Provider might look like:
|
|
||||||
```javascript
|
|
||||||
function mySpeedProvider(token, playerColor) {
|
|
||||||
const baseSpeed = token.actor.data.speed
|
|
||||||
const ranges = [{range: baseSpeed, color: playerColor}, {range: baseSpeed * 2, color: 0xFFFF00}]
|
|
||||||
if (!token.actor.data.isWearingArmor) {
|
|
||||||
ranges.push({range: baseSpeed * 3, color: 0xFF8000})
|
|
||||||
}
|
|
||||||
return ranges
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
With this speed provider, the fields reachable with the token's base speed will be colored in the color of the player that is dragging the token. Each token is able to dash, which allows it to run twice it's base speed. Spaces that are reachable by dashing will be colored yellow (`0xFFFF00`). If the token isn't wearing any armor it is also allowed to sprint, allowing it to cover a distance of 3 times it's base speed. Those squares will be colored in orange (`0xFF8000`). All spaces further away from the token will be colored in red. Tokens wearing armor aren't allowed to sprint and for them all spaces that exceed 2 times their base speed will be colored red.
|
|
||||||
|
|
||||||
### Full example
|
### Full example
|
||||||
This is a full example of how using the api might look like. It's built from the examples above. For a explanation to what is going on refer to the previous chapters.
|
```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
|
```javascript
|
||||||
Hooks.once("dragRuler.ready", () => {
|
get colors() {
|
||||||
dragRuler.registerModule("id-of-your-module", mySpeedProvider)
|
return [
|
||||||
})
|
{id: "walk", default: 0x00FF00, name: "my-module-id.speeds.walk"},
|
||||||
|
{id: "dash", default: 0xFFFF00, name: "my-module-id.speeds.dash"},
|
||||||
function mySpeedProvider(token, playerColor) {
|
{id: "run", default: 0xFF8000, name: "my-module-id.speeds.run"}
|
||||||
const baseSpeed = token.actor.data.speed
|
]
|
||||||
const ranges = [{range: baseSpeed, color: playerColor}, {range: baseSpeed * 2, color: 0xFFFF00}]
|
}
|
||||||
if (!token.actor.data.isWearingArmor) {
|
|
||||||
ranges.push({range: baseSpeed * 3, color: 0xFF8000})
|
|
||||||
}
|
|
||||||
return ranges
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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).
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"drag-ruler": {
|
||||||
|
"dependencies": {
|
||||||
|
"ok": "OK",
|
||||||
|
"terrain-ruler": {
|
||||||
|
"title": "Wie man schwieriges Gelände mit Drag Ruler misst",
|
||||||
|
"text": "Du hast das {moduleName} Modul installiert. Drag Ruler kann schwieriges Gelände, das mit diesem Modul platziert wurde in seinen Messungen berücksichtigen. Hierzu greift Drag Ruler auf die Funktionalität von Terrain Ruler zurück. Wenn Terrain Ruler installiert und aktiviert ist wird Drag Ruler automatisch anfangen schwiriges Gelände in den Messungen zu berücksichtigen.",
|
||||||
|
"neverShowAgain": "Zeige dies nie wieder an"
|
||||||
|
},
|
||||||
|
"socketlib": {
|
||||||
|
"title": "Socketlib Modul fehlt",
|
||||||
|
"text": "Drag Ruler benötigt das socketlib-Modul, um korrekt zu funktionieren. Bitte aktiviere das socketlib-Modul in dieser Welt."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resetMovementHistory": "Bewegungsverlauf zurücksetzen",
|
||||||
|
"genericSpeedProvider": {
|
||||||
|
"settings": {
|
||||||
|
"dashMultiplier": {
|
||||||
|
"name": "Sprint Multiplikator",
|
||||||
|
"hint": "Diese Einstellung kann genutzt werden, um eine zweite Geschwindigkeitsfarbe im gemessenen Pfad zu erhalten. Die zweite Farbe wird deaktiviert, wenn diese Einstellung auf 0 gesetzt ist."
|
||||||
|
},
|
||||||
|
"speedAttribute": {
|
||||||
|
"name": "Geschwindigkeitsattribut",
|
||||||
|
"hint": "Das Attribut, das die Gehgeschwindigkeit einer Spielfigur definiert. Diese Attribut wird während der Färbung des hervorgehobenen Pfads verwendet."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"speeds": {
|
||||||
|
"walk": "gehen",
|
||||||
|
"dash": "sprinten"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"alwaysShowSpeedForPCs": {
|
||||||
|
"name": "Geschwindigkeit von Spielercharakteren für jeden anzeigen",
|
||||||
|
"hint": "Wenn diese Einstellung aktiviert ist wird die Färbung der hervorgehobenen Felder bei Spielercharakteren allen Spielern angezeigt, selbst wenn diese keinen Zugriff auf den Charakterbogen haben."
|
||||||
|
},
|
||||||
|
"enableMovementHistory": {
|
||||||
|
"name": "Bewegungsverlauf während des Kampfes aktivieren",
|
||||||
|
"hint": "Wenn diesen Option aktiviert ist merkt sich Drag Ruler die Bewegungen, die während einer Runde eines Kampfes gemacht wurden und zeigt diese an, wenn das Token weiterbewegt wird."
|
||||||
|
},
|
||||||
|
"showGMRulerToPlayers": {
|
||||||
|
"name": "Lineal des Sipelleiters bei Spielern anzeigen",
|
||||||
|
"hint": "Wenn diese Option deaktiviert ist wird das Lineal des Spielleiters bei den anderen Spielern nicht angezeigt."
|
||||||
|
},
|
||||||
|
"speedProviderSettings": {
|
||||||
|
"name": "Einstellungen zur Spielsystemintegration",
|
||||||
|
"hint": "In diesem Menü befinden sich alle spielsystemspezifischen Einstellungen.",
|
||||||
|
"button": "Einstellungen zur Spielsystemintegration",
|
||||||
|
"windowTitle": "Einstellungen zur Spielsystemintegration",
|
||||||
|
"headers": {
|
||||||
|
"speedProvider": "Spielsystemintegration",
|
||||||
|
"speedProviderSettings": "Integrationsspezifische Einstellungen"
|
||||||
|
},
|
||||||
|
"activeProvider": {
|
||||||
|
"name": "Verwendete Spielsystemintegration",
|
||||||
|
"hint": "Die unten angezeigten Einstellungen hängen von der verwendeten Spielsystemintegration ab. Wenn der Spielleiter eine andere Spielsystemintegration auswählt können sich die zur Verfügung stehnden Einstellungen ändern."
|
||||||
|
},
|
||||||
|
"noSettings": "Diese Spielsystemintegration hat keine Einstellmöglichkeiten",
|
||||||
|
"color": {
|
||||||
|
"name": "Farbe für {colorName}",
|
||||||
|
"hint": "Die Farbe mit der Felder in {colorName} Reichweite hervorgehoben werden",
|
||||||
|
"unreachable": {
|
||||||
|
"name": "unerreichbar",
|
||||||
|
"hint": "Die Farbe für Felder, die von der bewegten Spielfigur nicht erreicht werden können"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"speedProvider": {
|
||||||
|
"name": "Verwendete Spielsystemintegration",
|
||||||
|
"hint": "Wähle aus, welche Spielsystemintegration für die farbliche Hervorhebung der Felder zuständig sein soll. Drag Ruler stellt eine generische Spielsystemintegration zur Verfügung, die grundlegende Funktionalität bietet und bei korrekter konfiguration mit allen Spielsystemen funktionieren sollte. Weitere Spielsystemintegrationen können durch Module und Spielsysteme angeboten werden. Eine andere Spielsystemintegration als die generische auszuwählen kann eine bessere Inegration in das verwendete Spielsystem bieten. Die Optionen unterhalb sind davon abhäng, welche Spielsystemintegration hier ausgewählt wurde.",
|
||||||
|
"choices": {
|
||||||
|
"module": "Modul {name}",
|
||||||
|
"native": "Generisch",
|
||||||
|
"system": "System {name}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"swapSpacebarRightClick": {
|
||||||
|
"name": "Leertaste und Rechtsklick tauschen",
|
||||||
|
"hint": "Die Funktionen der Leertaste und des Rechtsklicks sind während eine Spielfigur bewegt wird vertauscht. Wenn diese Option aktiviert wrid können mit Rechtsklick Wegpunkte gesetzt werden und mit der Leertaste werden sie wieder gelöscht."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+65
-17
@@ -1,31 +1,79 @@
|
|||||||
{
|
{
|
||||||
"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": {
|
||||||
|
"dashMultiplier": {
|
||||||
|
"name": "Dash Multiplier",
|
||||||
|
"hint": "This can be used to give tokens a secondary speed during coloring of the measured path. Set it to 0 to disable the secondary speed."
|
||||||
|
},
|
||||||
|
"speedAttribute": {
|
||||||
|
"name": "Speed Attribute",
|
||||||
|
"hint": "The attribute that defines a token's walking speed. This is used during coloring of the measured path."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"speeds": {
|
||||||
|
"walk": "walk",
|
||||||
|
"dash": "dash"
|
||||||
|
}
|
||||||
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"alwaysShowSpeedForPCs": {
|
"alwaysShowSpeedForPCs": {
|
||||||
"name": "Show PC speed to everyone",
|
"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."
|
"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."
|
||||||
},
|
},
|
||||||
"dashMultiplier": {
|
"enableMovementHistory": {
|
||||||
"name": "Dash Multiplier",
|
"name": "Enable movement history during combat",
|
||||||
"hint": "This can be used to give tokens a secondary speed during coloring of the measured path. Set it to 0 to disable the secondary speed."
|
"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."
|
||||||
},
|
},
|
||||||
"speedAttribute": {
|
"showGMRulerToPlayers": {
|
||||||
"name": "Speed Attribute",
|
"name": "Show GM ruler to players",
|
||||||
"hint": "The attribute that defines a token's walking speed. This is used during coloring of the measured path."
|
"hint": "If disabled the ruler of GMs will not be shown for non-GM players"
|
||||||
},
|
},
|
||||||
"speedProvider": {
|
"speedProviderSettings": {
|
||||||
"name": "Speed Settings Provider",
|
"name": "Speed Provider Settings",
|
||||||
"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.",
|
"hint": "The Speed Provider Settings contain all the game system specific settings.",
|
||||||
"choices": {
|
"button": "Speed Provider Settings",
|
||||||
"module": "Module",
|
"windowTitle": "Speed Provider Settings",
|
||||||
"native": "Drag Ruler",
|
"headers": {
|
||||||
"system": "System"
|
"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": {
|
||||||
|
"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.",
|
||||||
|
"choices": {
|
||||||
|
"module": "Module {name}",
|
||||||
|
"native": "Generic",
|
||||||
|
"system": "System {name}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"staticFirstColor": {
|
|
||||||
"name": "Static First Color",
|
|
||||||
"hint": "Use a static color for the first movement range instead of the player color"
|
|
||||||
},
|
|
||||||
"swapSpacebarRightClick": {
|
"swapSpacebarRightClick": {
|
||||||
"name": "Swap spacebar and right click",
|
"name": "Swap spacebar and right click",
|
||||||
"hint": "Swaps the functions of spacebar and right click during dragging. If enabled right click will place waypoints and spacebar will delete them"
|
"hint": "Swaps the functions of spacebar and right click during dragging. If enabled right click will place waypoints and spacebar will delete them"
|
||||||
|
|||||||
+49
-18
@@ -1,31 +1,62 @@
|
|||||||
{
|
{
|
||||||
"drag-ruler": {
|
"drag-ruler": {
|
||||||
|
"genericSpeedProvider": {
|
||||||
|
"settings": {
|
||||||
|
"dashMultiplier": {
|
||||||
|
"name": "早足の倍率",
|
||||||
|
"hint": "設定すると、移動経路の測定中に第二の色分けが追加されます。0にすると第二速度が無効になります。"
|
||||||
|
},
|
||||||
|
"speedAttribute": {
|
||||||
|
"name": "移動速度の属性値",
|
||||||
|
"hint": "コマの移動速度が定義された属性値を指定します。移動範囲表示の色分けに使用されます。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"speeds": {
|
||||||
|
"walk": "歩き",
|
||||||
|
"dash": "早足"
|
||||||
|
}
|
||||||
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"alwaysShowSpeedForPCs": {
|
"alwaysShowSpeedForPCs": {
|
||||||
"name": "プレイヤーキャラクターの移動速度を全員に公開する",
|
"name": "プレイヤーキャラクターの移動速度を全員に公開する",
|
||||||
"hint": "有効にすると、アクターの観察者(Observer)権限を持たない人にも,移動範囲の色分けが表示されるようになります。"
|
"hint": "有効にすると、アクターの観察者(Observer)権限を持たない人にも,移動範囲の色分けが表示されるようになります。"
|
||||||
},
|
},
|
||||||
"dashMultiplier": {
|
"showGMRulerToPlayers": {
|
||||||
"name": "早足の倍率",
|
"name": "GMのルーラーをプレイヤーに見せる",
|
||||||
"hint": "有効にすると、移動範囲に二つ目の色分けが追加されます。0にすると、この機能は無効になります。"
|
"hint": "無効の場合、GMのルーラーはプレイヤーに表示されません。"
|
||||||
},
|
},
|
||||||
"speedAttribute": {
|
"speedProviderSettings": {
|
||||||
"name": "移動速度の属性値",
|
"name": "移動速度の提供元",
|
||||||
"hint": "コマの移動速度が定義された属性値を指定します。移動範囲表示の色分けに使用されます。"
|
"hint": "ゲームシステムに固有のすべての設定をここで行います。",
|
||||||
},
|
"button": "移動速度の提供元設定",
|
||||||
"speedProvider": {
|
"windowTitle": "移動速度の提供元設定",
|
||||||
"name": "移動速度設定の提供元",
|
"headers": {
|
||||||
"hint": "色分けに使用する速度情報の提供元を選択します。ゲームシステムやモジュールを選択すると、Drag Ruler自体が提供するものより柔軟な色分けが行える可能性があります。",
|
"speedProvider": "移動速度の提供元",
|
||||||
"choices": {
|
"speedProviderSettings": "提供元に固有の設定"
|
||||||
"module": "モジュール",
|
},
|
||||||
"native": "Drag Ruler",
|
"activeProvider": {
|
||||||
"system": "ゲームシステム"
|
"name": "現在有効な提供元",
|
||||||
|
"hint": "以下の設定は、現在有効な移動速度の提供元によって変化します。GMが別の提供元を選択すると、利用可能な項目が変更となる場合があります。"
|
||||||
|
},
|
||||||
|
"noSettings": "この移動速度提供元には、設定オプションがありません。",
|
||||||
|
"color": {
|
||||||
|
"name": "{colorName}の色",
|
||||||
|
"hint": "{colorName}の範囲に使用するセルの色を指定します。",
|
||||||
|
"unreachable": {
|
||||||
|
"name": "到達不能域",
|
||||||
|
"hint": "ドラッグしたコマが到達できない範囲の色を指定します。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"speedProvider": {
|
||||||
|
"name": "移動速度の提供元",
|
||||||
|
"hint": "色分けに使用する速度情報の提供元を選択します。Drag Rulerは基本機能を備えた汎用の提供元を備えており、正しく設定すればすべてのゲームシステムで動作するはずです。しかしゲームシステムやモジュールを介することで、より多くの提供元を利用することもできます。こうした提供元を利用すると、ゲームルールとの統合がさらに向上するかもしれません。以下のオプションは、ここで選択した提供元によって変化します。",
|
||||||
|
"choices": {
|
||||||
|
"module": "モジュール {name}",
|
||||||
|
"native": "汎用",
|
||||||
|
"system": "システム {name}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"staticFirstColor": {
|
|
||||||
"name": "一つ目の色を固定する",
|
|
||||||
"hint": "一つ目の移動範囲の色に、プレイヤーの色でなく固定された色を使用します。"
|
|
||||||
},
|
|
||||||
"swapSpacebarRightClick": {
|
"swapSpacebarRightClick": {
|
||||||
"name": "スペースキーと右クリックを入れ替える",
|
"name": "スペースキーと右クリックを入れ替える",
|
||||||
"hint": "ドラッグ中のスペースキーと右クリックの機能を入れ替えます。有効にすると右クリックがウェイポイントの配置、スペースキーが削除になります。"
|
"hint": "ドラッグ中のスペースキーと右クリックの機能を入れ替えます。有効にすると右クリックがウェイポイントの配置、スペースキーが削除になります。"
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"drag-ruler": {
|
||||||
|
"genericSpeedProvider": {
|
||||||
|
"settings": {
|
||||||
|
"dashMultiplier": {
|
||||||
|
"name": "快步 倍數",
|
||||||
|
"hint": "這可用於在對測量路徑進行著色的過程中為棋子賦予第二速度。將其設定為0可禁用第二速度。"
|
||||||
|
},
|
||||||
|
"speedAttribute": {
|
||||||
|
"name": "速度屬性",
|
||||||
|
"hint": "定義棋子的行走速度的屬性。在對測量路徑進行著色時使用。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"speeds": {
|
||||||
|
"walk": "走路",
|
||||||
|
"dash": "快步"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"alwaysShowSpeedForPCs": {
|
||||||
|
"name": "向所有人顯示PC速度",
|
||||||
|
"hint": "如果啟用, 玩家的角色(actor)的速度將顯示給所有人,即使他們沒有角色表的觀察者(Observer)權限也是如此。"
|
||||||
|
},
|
||||||
|
"showGMRulerToPlayers": {
|
||||||
|
"name": "向玩家顯示GM標尺",
|
||||||
|
"hint": "如果禁用,則不為非GM玩家顯示GM的標尺"
|
||||||
|
},
|
||||||
|
"speedProviderSettings": {
|
||||||
|
"name": "速度提供者設定",
|
||||||
|
"hint": "速度提供者設定 包括 所有遊戲系統特定的設定。",
|
||||||
|
"button": "速度提供者設定",
|
||||||
|
"windowTitle": "速度提供者設定",
|
||||||
|
"headers": {
|
||||||
|
"speedProvider": "速度提供者設定",
|
||||||
|
"speedProviderSettings": "特定於速度提供者的設定"
|
||||||
|
},
|
||||||
|
"activeProvider": {
|
||||||
|
"name": "當前活動的速度提供者",
|
||||||
|
"hint": "下面顯示的設定 取決於活動的 速度提供者。如果GM選擇其他速度提供者,則可用的設定可能會更改。"
|
||||||
|
},
|
||||||
|
"noSettings": "該速度提供者不提供任何配置選項。",
|
||||||
|
"color": {
|
||||||
|
"name": "{colorName}的顏色",
|
||||||
|
"hint": "將用於為{colorName}範圍內的正方形上色的顏色",
|
||||||
|
"unreachable": {
|
||||||
|
"name": "無法到達",
|
||||||
|
"hint": "指定拖動棋子無法到達的範圍的顏色"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"speedProvider": {
|
||||||
|
"name": "速度設定的提供者",
|
||||||
|
"hint": "選擇在著色期間為棋子提供速度信息的人。DragRuler提供了一個通用速度提供程序, 該提供程序提供了基本功能, 並且如果配置正確,則可以在所有遊戲系統上使用。可以通過遊戲系統和已安裝的Mod來提供更多速度提供程序。選擇與 標準速度提供者 不同的速度提供者可以更好地集成到您所使用的遊戲系統規則中。以下選項取決於此處選擇的速度提供者。",
|
||||||
|
"choices": {
|
||||||
|
"module": "Mod {name}",
|
||||||
|
"native": "一般",
|
||||||
|
"system": "系統 {name}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"swapSpacebarRightClick": {
|
||||||
|
"name": "交換空格鍵並鼠標右鍵",
|
||||||
|
"hint": "交換空格鍵和右鍵點擊在拖動過程中的功能。如果啟用,右鍵單擊將放置航點,空格鍵將刪除它們。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
-4
@@ -2,7 +2,7 @@
|
|||||||
"name": "drag-ruler",
|
"name": "drag-ruler",
|
||||||
"title": "Drag Ruler",
|
"title": "Drag Ruler",
|
||||||
"description": "When dragging a token displays a ruler showing how far you've moved that token.",
|
"description": "When dragging a token displays a ruler showing how far you've moved that token.",
|
||||||
"version": "1.2.2",
|
"version": "1.6.2",
|
||||||
"minimumCoreVersion" : "0.7.9",
|
"minimumCoreVersion" : "0.7.9",
|
||||||
"compatibleCoreVersion" : "0.7.9",
|
"compatibleCoreVersion" : "0.7.9",
|
||||||
"authors": [
|
"authors": [
|
||||||
@@ -13,9 +13,18 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"esmodules": [
|
"esmodules": [
|
||||||
"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",
|
||||||
@@ -25,12 +34,25 @@
|
|||||||
"lang": "ja",
|
"lang": "ja",
|
||||||
"name": "日本語",
|
"name": "日本語",
|
||||||
"path": "lang/ja.json"
|
"path": "lang/ja.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lang": "zh-tw",
|
||||||
|
"name": "正體中文",
|
||||||
|
"path": "lang/zh-tw.json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"dependencies": [
|
||||||
|
{
|
||||||
|
"name": "socketlib",
|
||||||
|
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-socketlib/master/module.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"socket": true,
|
||||||
"url": "https://github.com/manuelVo/foundryvtt-drag-ruler",
|
"url": "https://github.com/manuelVo/foundryvtt-drag-ruler",
|
||||||
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/v1.2.2.zip",
|
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/v1.6.2.zip",
|
||||||
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/master/module.json",
|
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/master/module.json",
|
||||||
"readme": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/README.md",
|
"readme": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/README.md",
|
||||||
"changelog": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/CHANGELOG.md",
|
"changelog": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/CHANGELOG.md",
|
||||||
"bugs": "https://github.com/manuelVo/foundryvtt-drag-ruler/issues"
|
"bugs": "https://github.com/manuelVo/foundryvtt-drag-ruler/issues",
|
||||||
|
"allowBugReporter": true
|
||||||
}
|
}
|
||||||
|
|||||||
+88
-16
@@ -1,37 +1,64 @@
|
|||||||
|
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 providerSetting = game.settings.settings.get("drag-ruler.speedProvider")
|
const id = `${type}.${module.id}`
|
||||||
|
let providerInstance
|
||||||
|
if (speedProvider.prototype instanceof SpeedProvider) {
|
||||||
|
providerInstance = new speedProvider(id)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
speedProvider.id = id
|
||||||
|
speedProvider.usesRuler = () => true
|
||||||
|
providerInstance = speedProvider
|
||||||
|
}
|
||||||
|
setupProvider(providerInstance)
|
||||||
|
}
|
||||||
|
|
||||||
// Add the registered module to the settings entry
|
function setupProvider(speedProvider) {
|
||||||
providerSetting.config = true
|
if (speedProvider instanceof SpeedProvider) {
|
||||||
const moduleName = module.data.title
|
const unreachableColor = {id: "unreachable", default: speedProvider.defaultUnreachableColor, name: "drag-ruler.settings.speedProviderSettings.color.unreachable.name"}
|
||||||
const typeTitle = game.i18n.localize(`drag-ruler.settings.speedProvider.choices.${type}`)
|
for (const color of speedProvider.colors.concat([unreachableColor])) {
|
||||||
providerSetting.choices[`${type}.${module.id}`] = `${typeTitle} ${moduleName}`
|
game.settings.register(settingsKey, `speedProviders.${speedProvider.id}.color.${color.id}`, {
|
||||||
availableSpeedProviders[`${type}.${module.id}`] = speedProvider
|
config: false,
|
||||||
providerSetting.default = getDefaultSpeedProvider()
|
scope: "client",
|
||||||
|
type: Number,
|
||||||
|
default: color.default,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for (const setting of speedProvider.settings) {
|
||||||
|
setting.config = false
|
||||||
|
game.settings.register(settingsKey, `speedProviders.${speedProvider.id}.setting.${setting.id}`, setting)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
availableSpeedProviders[speedProvider.id] = speedProvider
|
||||||
|
game.settings.settings.get("drag-ruler.speedProvider").default = getDefaultSpeedProvider()
|
||||||
updateSpeedProvider()
|
updateSpeedProvider()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultSpeedProvider() {
|
export function getDefaultSpeedProvider() {
|
||||||
const providerSetting = game.settings.settings.get("drag-ruler.speedProvider")
|
const providerIds = Object.keys(availableSpeedProviders)
|
||||||
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 = settingKeys.find(key => key.startsWith("system."))
|
const gameSystem = providerIds.find(key => key.startsWith("system."))
|
||||||
if (gameSystem)
|
if (gameSystem)
|
||||||
return gameSystem
|
return gameSystem
|
||||||
|
|
||||||
// If no game system is registered modules are next up.
|
// If no game system is registered modules are next up.
|
||||||
// For lack of a method to select the best module we're just falling back to taking the next best module
|
// For lack of a method to select the best module we're just falling back to taking the next best module
|
||||||
// settingKeys should always be sorted the same way so this should achive a stable default
|
// settingKeys should always be sorted the same way so this should achive a stable default
|
||||||
const module = settingKeys.find(key => key.startsWith("module."))
|
const module = providerIds.find(key => key.startsWith("module."))
|
||||||
if (module)
|
if (module)
|
||||||
return module
|
return module
|
||||||
|
|
||||||
// If neither a game system or a module is found fall back to the native implementation
|
// If neither a game system or a module is found fall back to the native implementation
|
||||||
return settingKeys[0]
|
return providerIds[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateSpeedProvider() {
|
export function updateSpeedProvider() {
|
||||||
@@ -40,8 +67,53 @@ 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 setCurrentSpeedProvider(newSpeedProvider) {
|
export function initApi() {
|
||||||
currentSpeedProvider = newSpeedProvider
|
const genericSpeedProviderInstance = new GenericSpeedProvider("native")
|
||||||
|
setupProvider(genericSpeedProviderInstance)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRangesFromSpeedProvider(token) {
|
||||||
|
try {
|
||||||
|
if (currentSpeedProvider instanceof Function)
|
||||||
|
return currentSpeedProvider(token, 0x00FF00)
|
||||||
|
const ranges = currentSpeedProvider.getRanges(token)
|
||||||
|
for (const range of ranges) {
|
||||||
|
range.color = game.settings.get(settingsKey, `speedProviders.${currentSpeedProvider.id}.color.${range.color}`)
|
||||||
|
}
|
||||||
|
return ranges
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUnreachableColorFromSpeedProvider() {
|
||||||
|
if (currentSpeedProvider instanceof Function)
|
||||||
|
return 0xFF0000
|
||||||
|
try {
|
||||||
|
return game.settings.get(settingsKey, `speedProviders.${currentSpeedProvider.id}.color.unreachable`)
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
return 0xFF0000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCostFromSpeedProvider(token, area) {
|
||||||
|
if (currentSpeedProvider instanceof Function) {
|
||||||
|
return SpeedProvider.prototype.getCostForStep.call(undefined, token, area);
|
||||||
|
}
|
||||||
|
return currentSpeedProvider.getCostForStep(token, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMovedDistanceFromToken(token) {
|
||||||
|
const history = getMovementHistory(token);
|
||||||
|
const segments = Ruler.dragRulerGetRaysFromWaypoints(history, {x: token.x, y: token.y}).map(ray => {return {ray}});
|
||||||
|
const shape = getTokenShape(token);
|
||||||
|
const distances = measureDistances(segments, token, shape);
|
||||||
|
// Sum up the distances
|
||||||
|
return distances.reduce((acc, val) => acc + val, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerModule(moduleId, speedProvider) {
|
export function registerModule(moduleId, speedProvider) {
|
||||||
|
|||||||
+80
-13
@@ -1,19 +1,86 @@
|
|||||||
|
import {getCostFromSpeedProvider} from "./api.js";
|
||||||
|
import {getColorForDistance} from "./main.js"
|
||||||
|
import {settingsKey} from "./settings.js";
|
||||||
|
import {getAreaFromPositionAndShape, highlightTokenShape} from "./util.js";
|
||||||
|
|
||||||
export function getHexSizeSupportTokenGridCenter(token) {
|
export function getHexSizeSupportTokenGridCenter(token) {
|
||||||
const tokenCenterOffset = CONFIG.hexSizeSupport.getCenterOffset(token)
|
const tokenCenterOffset = CONFIG.hexSizeSupport.getCenterOffset(token)
|
||||||
const tokenCenter = {x: token.x + tokenCenterOffset.x, y: token.y + tokenCenterOffset.y}
|
return {x: token.x + tokenCenterOffset.x, y: token.y + tokenCenterOffset.y}
|
||||||
if (token.getFlag("hex-size-support", "borderSize") % 2 === 1)
|
}
|
||||||
return tokenCenter
|
|
||||||
if (canvas.grid.grid.columns) {
|
export function highlightMeasurementTerrainRuler(ray, startDistance, tokenShape=[{x: 0, y: 0}], alpha=1) {
|
||||||
let hexOffset = canvas.grid.w / 2
|
for (const space of ray.terrainRulerVisitedSpaces.reverse()) {
|
||||||
if (!CONFIG.hexSizeSupport.getAltOrientationFlag(token))
|
const color = getColorForDistance.call(this, startDistance, space.distance)
|
||||||
hexOffset *= -1
|
highlightTokenShape.call(this, space, tokenShape, color, alpha)
|
||||||
tokenCenter.x += hexOffset
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function measureDistances(segments, token, shape, gridSpaces=true, options={}) {
|
||||||
|
const opts = duplicate(options)
|
||||||
|
opts.gridSpaces = gridSpaces;
|
||||||
|
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS;
|
||||||
|
if (terrainRulerAvailable) {
|
||||||
|
const firstNewSegmentIndex = segments.findIndex(segment => !segment.ray.dragRulerVisitedSpaces);
|
||||||
|
const previousSegments = segments.slice(0, firstNewSegmentIndex);
|
||||||
|
const newSegments = segments.slice(firstNewSegmentIndex);
|
||||||
|
const distances = previousSegments.map(segment => segment.ray.dragRulerVisitedSpaces[segment.ray.dragRulerVisitedSpaces.length - 1].distance);
|
||||||
|
previousSegments.forEach(segment => segment.ray.terrainRulerVisitedSpaces = duplicate(segment.ray.dragRulerVisitedSpaces));
|
||||||
|
opts.costFunction = (x, y) => getCostFromSpeedProvider(token, getAreaFromPositionAndShape({x, y}, shape), {x, y});
|
||||||
|
if (previousSegments.length > 0)
|
||||||
|
opts.terrainRulerInitialState = previousSegments[previousSegments.length - 1];
|
||||||
|
return distances.concat(terrainRuler.measureDistances(newSegments, opts));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let hexOffset = canvas.grid.h / 2
|
return canvas.grid.measureDistances(segments, opts);
|
||||||
if (CONFIG.hexSizeSupport.getAltOrientationFlag(token))
|
|
||||||
hexOffset *= -1
|
|
||||||
tokenCenter.y += hexOffset
|
|
||||||
}
|
}
|
||||||
return tokenCenter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function checkDependencies() {
|
||||||
|
if (!game.modules.get("socketlib")?.active) {
|
||||||
|
console.error("Drag Ruler | The `socketlib` module isn't enabled, but it's required for Drag Ruler to operate properly.");
|
||||||
|
if (game.user.isGM) {
|
||||||
|
new Dialog({
|
||||||
|
title: game.i18n.localize("drag-ruler.dependencies.socketlib.title"),
|
||||||
|
content: `<h2>${game.i18n.localize("drag-ruler.dependencies.socketlib.title")}</h2><p>${game.i18n.localize("drag-ruler.dependencies.socketlib.text")}</p>`,
|
||||||
|
buttons: {
|
||||||
|
ok: {
|
||||||
|
icon: '<i class="fas fa-check"></i>',
|
||||||
|
label: game.i18n.localize("drag-ruler.dependencies.ok")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}).render(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!game.modules.get("terrain-ruler")?.active && game.user.isGM && !game.settings.get(settingsKey, "neverShowTerrainRulerHint")) {
|
||||||
|
const lastHint = game.settings.get(settingsKey, "lastTerrainRulerHintTime");
|
||||||
|
if (Date.now() - lastHint > 604800000) { // One week
|
||||||
|
let enabledTerrainModule;
|
||||||
|
if (game.modules.get("enhanced-terrain-layer")?.active) {
|
||||||
|
enabledTerrainModule = game.modules.get("enhanced-terrain-layer").data.title;
|
||||||
|
}
|
||||||
|
else if (game.modules.get("TerrainLayer")?.active) {
|
||||||
|
enabledTerrainModule = game.modules.get("TerrainLayer").data.title;
|
||||||
|
}
|
||||||
|
if (enabledTerrainModule) {
|
||||||
|
new Dialog({
|
||||||
|
title: game.i18n.localize("drag-ruler.dependencies.terrain-ruler.title"),
|
||||||
|
content: `<h2>${game.i18n.localize("drag-ruler.dependencies.terrain-ruler.title")}</h2><p>${game.i18n.format("drag-ruler.dependencies.terrain-ruler.text", {moduleName: enabledTerrainModule})}</p>`,
|
||||||
|
buttons: {
|
||||||
|
ok: {
|
||||||
|
icon: '<i class="fas fa-check"></i>',
|
||||||
|
label: game.i18n.localize("drag-ruler.dependencies.ok"),
|
||||||
|
callback: () => game.settings.set(settingsKey, "lastTerrainRulerHintTime", Date.now()),
|
||||||
|
},
|
||||||
|
neverShowAgain: {
|
||||||
|
icon: '<i class="fas fa-times"></i>',
|
||||||
|
label: game.i18n.localize("drag-ruler.dependencies.terrain-ruler.neverShowAgain"),
|
||||||
|
callback: () => game.settings.set(settingsKey, "neverShowTerrainRulerHint", true),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close: () => game.settings.set(settingsKey, "lastTerrainRulerHintTime", Date.now())
|
||||||
|
}).render(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// 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.isHex) {
|
||||||
|
return canvas.grid.grid.getPixelsFromGridPosition(yGrid, xGrid)
|
||||||
|
}
|
||||||
|
const [x, y] = canvas.grid.grid.getPixelsFromGridPosition(xGrid, yGrid)
|
||||||
|
if (canvas.grid.type === CONST.GRID_TYPES.SQUARE)
|
||||||
|
return [y, x]
|
||||||
|
return [x, y]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]
|
||||||
|
}
|
||||||
+129
-41
@@ -1,4 +1,8 @@
|
|||||||
|
import {highlightMeasurementTerrainRuler, measureDistances} from "./compatibility.js";
|
||||||
|
import {getGridPositionFromPixels} from "./foundry_fixes.js";
|
||||||
import {getColorForDistance} from "./main.js"
|
import {getColorForDistance} from "./main.js"
|
||||||
|
import {trackRays} from "./movement_tracking.js"
|
||||||
|
import {applyTokenSizeOffset, getSnapPointForToken, getTokenShape, highlightTokenShape, zip} from "./util.js";
|
||||||
|
|
||||||
// This is a modified version of Ruler.moveToken from foundry 0.7.9
|
// This is a modified version of Ruler.moveToken from foundry 0.7.9
|
||||||
export async function moveTokens(draggedToken, selectedTokens) {
|
export async function moveTokens(draggedToken, selectedTokens) {
|
||||||
@@ -12,11 +16,11 @@ export async function moveTokens(draggedToken, selectedTokens) {
|
|||||||
|
|
||||||
// Get the movement rays and check collision along each Ray
|
// Get the movement rays and check collision along each Ray
|
||||||
// These rays are center-to-center for the purposes of collision checking
|
// These rays are center-to-center for the purposes of collision checking
|
||||||
const rays = this._getRaysFromWaypoints(this.waypoints, this.destination);
|
const rays = this.constructor.dragRulerGetRaysFromWaypoints(this.waypoints, this.destination);
|
||||||
if (!game.user.isGM) {
|
if (!game.user.isGM) {
|
||||||
const hasCollision = selectedTokens.some(token => {
|
const hasCollision = selectedTokens.some(token => {
|
||||||
const offset = calculateTokenOffset(token, draggedToken)
|
const offset = calculateTokenOffset(token, draggedToken)
|
||||||
const offsetRays = rays.map(ray => applyOffsetToRay(ray, offset))
|
const offsetRays = rays.filter(ray => !ray.isPrevious).map(ray => applyOffsetToRay(ray, offset))
|
||||||
return offsetRays.some(r => canvas.walls.checkCollision(r));
|
return offsetRays.some(r => canvas.walls.checkCollision(r));
|
||||||
})
|
})
|
||||||
if (hasCollision) {
|
if (hasCollision) {
|
||||||
@@ -29,42 +33,61 @@ export async function moveTokens(draggedToken, selectedTokens) {
|
|||||||
// Execute the movement path.
|
// Execute the movement path.
|
||||||
// Transform each center-to-center ray into a top-left to top-left ray using the prior token offsets.
|
// Transform each center-to-center ray into a top-left to top-left ray using the prior token offsets.
|
||||||
this._state = Ruler.STATES.MOVING;
|
this._state = Ruler.STATES.MOVING;
|
||||||
await Promise.all(selectedTokens.map(token => {
|
await animateTokens.call(this, selectedTokens, draggedToken, rays, wasPaused);
|
||||||
// Return the promise so we can wait for it outside the loop
|
|
||||||
const offset = calculateTokenOffset(token, draggedToken)
|
|
||||||
return animateToken.call(this, token, rays, offset, wasPaused)
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Once all animations are complete we can clear the ruler
|
// Once all animations are complete we can clear the ruler
|
||||||
this._endMeasurement();
|
if (this.draggedToken?.id === draggedToken.id)
|
||||||
|
this._endMeasurement();
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a modified version code extracted from Ruler.moveToken from foundry 0.7.9
|
// This is a modified version code extracted from Ruler.moveToken from foundry 0.7.9
|
||||||
async function animateToken(token, rays, tokenOffset, wasPaused) {
|
async function animateTokens(tokens, draggedToken, draggedRays, wasPaused) {
|
||||||
const offsetRays = rays.map(ray => applyOffsetToRay(ray, tokenOffset))
|
const newRays = draggedRays.filter(r => !r.isPrevious);
|
||||||
|
const tokenAnimationData = tokens.map(token => {
|
||||||
|
const tokenOffset = calculateTokenOffset(token, draggedToken);
|
||||||
|
const offsetRays = newRays.map(ray => applyOffsetToRay(ray, tokenOffset));
|
||||||
|
|
||||||
// Determine offset relative to the Token top-left.
|
// 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.
|
// This is important so we can position the token relative to the ruler origin for non-1x1 tokens.
|
||||||
const origin = [this.waypoints[0].x + tokenOffset.x, this.waypoints[0].y + tokenOffset.y]
|
const firstWaypoint = this.waypoints.find(w => !w.isPrevious);
|
||||||
let dx, dy
|
const origin = [firstWaypoint.x + tokenOffset.x, firstWaypoint.y + tokenOffset.y];
|
||||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
let dx, dy;
|
||||||
dx = token.data.x - origin[0]
|
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||||
dy = token.data.y - origin[1]
|
dx = token.data.x - origin[0];
|
||||||
}
|
dy = token.data.y - origin[1];
|
||||||
else {
|
}
|
||||||
dx = token.data.x - origin[0]
|
else {
|
||||||
dy = token.data.y - origin[1]
|
dx = token.data.x - origin[0];
|
||||||
}
|
dy = token.data.y - origin[1];
|
||||||
|
}
|
||||||
|
|
||||||
token._noAnimate = true;
|
return {token, rays: offsetRays, dx, dy};
|
||||||
for (let r of offsetRays) {
|
});
|
||||||
|
|
||||||
|
for (const {token, rays} of tokenAnimationData) {
|
||||||
|
token._noAnimate = true;
|
||||||
|
}
|
||||||
|
const animate = !game.keyboard.isDown("Alt");
|
||||||
|
const startWaypoint = animate ? 0 : tokenAnimationData[0].rays.length - 1;
|
||||||
|
for (let i = startWaypoint;i < tokenAnimationData[0].rays.length; i++) {
|
||||||
if (!wasPaused && game.paused) break;
|
if (!wasPaused && game.paused) break;
|
||||||
const dest = [r.B.x, r.B.y];
|
const tokenPaths = tokenAnimationData.map(({token, rays, dx, dy}) => {
|
||||||
const path = new Ray({ x: token.x, y: token.y }, { x: dest[0] + dx, y: dest[1] + dy });
|
const ray = rays[i];
|
||||||
await token.update(path.B);
|
const dest = [ray.B.x, ray.B.y];
|
||||||
await token.animateMovement(path);
|
const path = new Ray({x: token.x, y: token.y}, {x: dest[0] + dx, y: dest[1] + dy});
|
||||||
|
return {token, path};
|
||||||
|
});
|
||||||
|
const updates = tokenPaths.map(({token, path}) => {
|
||||||
|
return {x: path.B.x, y: path.B.y, _id: token.id};
|
||||||
|
});
|
||||||
|
await draggedToken.scene.updateEmbeddedEntity(draggedToken.constructor.embeddedName, updates, {animate});
|
||||||
|
if (animate)
|
||||||
|
await Promise.all(tokenPaths.map(({token, path}) => token.animateMovement(path)));
|
||||||
}
|
}
|
||||||
token._noAnimate = false;
|
for (const {token} of tokenAnimationData) {
|
||||||
|
token._noAnimate = false;
|
||||||
|
}
|
||||||
|
trackRays(tokenAnimationData.map(({token}) => token), tokenAnimationData.map(({rays}) => rays));
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateTokenOffset(tokenA, tokenB) {
|
function calculateTokenOffset(tokenA, tokenB) {
|
||||||
@@ -72,7 +95,9 @@ function calculateTokenOffset(tokenA, tokenB) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyOffsetToRay(ray, offset) {
|
function applyOffsetToRay(ray, offset) {
|
||||||
return new Ray({x: ray.A.x + offset.x, y: ray.A.y + offset.y}, {x: ray.B.x + offset.x, y: ray.B.y + offset.y})
|
const newRay = new Ray({x: ray.A.x + offset.x, y: ray.A.y + offset.y}, {x: ray.B.x + offset.x, y: ray.B.y + offset.y});
|
||||||
|
newRay.isPrevious = ray.isPrevious;
|
||||||
|
return newRay;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a modified version of Ruler._onMouseMove from foundry 0.7.9
|
// This is a modified version of Ruler._onMouseMove from foundry 0.7.9
|
||||||
@@ -102,9 +127,17 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) {
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
if (snap)
|
if (snap)
|
||||||
destination = new PIXI.Point(...canvas.grid.getCenter(destination.x, destination.y));
|
destination = getSnapPointForToken(destination.x, destination.y, this.draggedToken)
|
||||||
|
|
||||||
|
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS;
|
||||||
|
|
||||||
const waypoints = this.waypoints.concat([destination]);
|
const waypoints = this.waypoints.concat([destination]);
|
||||||
const centeredWaypoints = waypoints.map(w => new PIXI.Point(...canvas.grid.getCenter(w.x, w.y)))
|
// Move the waypoints to the center of the grid if a size is used that measures from edge to edge
|
||||||
|
const centeredWaypoints = applyTokenSizeOffset(waypoints, this.draggedToken)
|
||||||
|
// Foundries native ruler requires the waypoints to sit in the dead center of the square to work properly
|
||||||
|
if (!terrainRulerAvailable)
|
||||||
|
centeredWaypoints.forEach(w => [w.x, w.y] = canvas.grid.getCenter(w.x, w.y));
|
||||||
|
|
||||||
const r = this.ruler;
|
const r = this.ruler;
|
||||||
this.destination = destination;
|
this.destination = destination;
|
||||||
|
|
||||||
@@ -118,6 +151,10 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) {
|
|||||||
const label = this.labels.children[i];
|
const label = this.labels.children[i];
|
||||||
const ray = new Ray(origin, dest);
|
const ray = new Ray(origin, dest);
|
||||||
const centeredRay = new Ray(centeredOrigin, centeredDest)
|
const centeredRay = new Ray(centeredOrigin, centeredDest)
|
||||||
|
ray.isPrevious = Boolean(origin.isPrevious);
|
||||||
|
centeredRay.isPrevious = ray.isPrevious;
|
||||||
|
ray.dragRulerVisitedSpaces = origin.dragRulerVisitedSpaces;
|
||||||
|
centeredRay.dragRulerVisitedSpaces = ray.dragRulerVisitedSpaces;
|
||||||
if (ray.distance < 10) {
|
if (ray.distance < 10) {
|
||||||
if (label) label.visible = false;
|
if (label) label.visible = false;
|
||||||
continue;
|
continue;
|
||||||
@@ -126,14 +163,18 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) {
|
|||||||
centeredSegments.push({ray: centeredRay, label})
|
centeredSegments.push({ray: centeredRay, label})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const shape = getTokenShape(this.draggedToken)
|
||||||
|
|
||||||
// Compute measured distance
|
// Compute measured distance
|
||||||
const distances = canvas.grid.measureDistances(centeredSegments, { gridSpaces });
|
const distances = measureDistances(centeredSegments, this.draggedToken, shape, gridSpaces);
|
||||||
|
|
||||||
let totalDistance = 0;
|
let totalDistance = 0;
|
||||||
for (let [i, d] of distances.entries()) {
|
for (let [i, d] of distances.entries()) {
|
||||||
let s = segments[i];
|
let s = centeredSegments[i];
|
||||||
s.startDistance = totalDistance
|
s.startDistance = totalDistance
|
||||||
totalDistance += d;
|
totalDistance += d;
|
||||||
s.last = i === (segments.length - 1);
|
s.last = i === (centeredSegments.length - 1);
|
||||||
s.distance = d;
|
s.distance = d;
|
||||||
s.text = this._getSegmentLabel(d, totalDistance, s.last);
|
s.text = this._getSegmentLabel(d, totalDistance, s.last);
|
||||||
}
|
}
|
||||||
@@ -149,24 +190,28 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) {
|
|||||||
rulerColor = getColorForDistance.call(this, totalDistance)
|
rulerColor = getColorForDistance.call(this, totalDistance)
|
||||||
else
|
else
|
||||||
rulerColor = this.color
|
rulerColor = this.color
|
||||||
for (let s of segments) {
|
for (const [s, cs] of zip(segments.reverse(), centeredSegments.reverse())) {
|
||||||
const { ray, label, text, last } = s;
|
const { label, text, last } = cs;
|
||||||
|
|
||||||
// Draw line segment
|
// Draw line segment
|
||||||
r.lineStyle(6, 0x000000, 0.5).moveTo(ray.A.x, ray.A.y).lineTo(ray.B.x, ray.B.y)
|
const opacityMultiplier = s.ray.isPrevious ? 0.33 : 1;
|
||||||
.lineStyle(4, rulerColor, 0.25).moveTo(ray.A.x, ray.A.y).lineTo(ray.B.x, ray.B.y);
|
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
|
// Draw the distance label just after the endpoint of the segment
|
||||||
if (label) {
|
if (label) {
|
||||||
label.text = text;
|
label.text = text;
|
||||||
label.alpha = last ? 1.0 : 0.5;
|
label.alpha = last ? 1.0 : 0.5;
|
||||||
label.visible = true;
|
label.visible = true;
|
||||||
let labelPosition = ray.project((ray.distance + 50) / ray.distance);
|
let labelPosition = cs.ray.project((cs.ray.distance + 50) / cs.ray.distance);
|
||||||
label.position.set(labelPosition.x, labelPosition.y);
|
label.position.set(labelPosition.x, labelPosition.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Highlight grid positions
|
// Highlight grid positions
|
||||||
this._highlightMeasurement(ray, s.startDistance);
|
if (terrainRulerAvailable)
|
||||||
|
highlightMeasurementTerrainRuler.call(this, cs.ray, cs.startDistance, shape, opacityMultiplier)
|
||||||
|
else
|
||||||
|
highlightMeasurementNative.call(this, cs.ray, cs.startDistance, shape, opacityMultiplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw endpoints
|
// Draw endpoints
|
||||||
@@ -177,3 +222,46 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) {
|
|||||||
// Return the measured segments
|
// Return the measured segments
|
||||||
return segments;
|
return segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function highlightMeasurementNative(ray, startDistance, tokenShape=[{x: 0, y: 0}], alpha=1) {
|
||||||
|
const spacer = canvas.scene.data.gridType === CONST.GRID_TYPES.SQUARE ? 1.41 : 1;
|
||||||
|
const nMax = Math.max(Math.floor(ray.distance / (spacer * Math.min(canvas.grid.w, canvas.grid.h))), 1);
|
||||||
|
const tMax = Array.fromRange(nMax+1).map(t => t / nMax);
|
||||||
|
|
||||||
|
// Track prior position
|
||||||
|
let prior = null;
|
||||||
|
|
||||||
|
// Iterate over ray portions
|
||||||
|
for ( let [i, t] of tMax.reverse().entries() ) {
|
||||||
|
let {x, y} = ray.project(t);
|
||||||
|
|
||||||
|
// Get grid position
|
||||||
|
let [x0, y0] = (i === 0) ? [null, null] : prior;
|
||||||
|
let [x1, y1] = canvas.grid.grid.getGridPositionFromPixels(x, y);
|
||||||
|
if ( x0 === x1 && y0 === y1 ) continue;
|
||||||
|
|
||||||
|
// Highlight the grid position
|
||||||
|
let [xg, yg] = canvas.grid.grid.getPixelsFromGridPosition(x1, y1);
|
||||||
|
const subDistance = canvas.grid.measureDistances([{ray: new Ray(ray.A, {x: xg, y: yg})}], {gridSpaces: true})[0]
|
||||||
|
const color = dragRuler.getColorForDistance.call(this, startDistance, subDistance)
|
||||||
|
const snapPoint = getSnapPointForToken(...canvas.grid.getTopLeft(x, y), this.draggedToken);
|
||||||
|
const [snapX, snapY] = getGridPositionFromPixels(snapPoint.x + 1, snapPoint.y + 1);
|
||||||
|
|
||||||
|
prior = [x1, y1];
|
||||||
|
|
||||||
|
// If the positions are not neighbors, also highlight their halfway point
|
||||||
|
if (i > 0 && !canvas.grid.isNeighbor(x0, y0, x1, y1)) {
|
||||||
|
let th = tMax[i - 1] - (0.5 / nMax);
|
||||||
|
let {x, y} = ray.project(th);
|
||||||
|
let [x1h, y1h] = canvas.grid.grid.getGridPositionFromPixels(x, y);
|
||||||
|
let [xgh, ygh] = canvas.grid.grid.getPixelsFromGridPosition(x1h, y1h);
|
||||||
|
const subDistance = canvas.grid.measureDistances([{ray: new Ray(ray.A, {x: xgh, y: ygh})}], {gridSpaces: true})[0]
|
||||||
|
const color = dragRuler.getColorForDistance.call(this, startDistance, subDistance)
|
||||||
|
const snapPoint = getSnapPointForToken(...canvas.grid.getTopLeft(x, y), this.draggedToken);
|
||||||
|
const [snapX, snapY] = getGridPositionFromPixels(snapPoint.x + 1, snapPoint.y + 1);
|
||||||
|
highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color, alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+46
-144
@@ -1,30 +1,35 @@
|
|||||||
"use strict"
|
"use strict"
|
||||||
|
|
||||||
import {availableSpeedProviders, currentSpeedProvider, registerModule, registerSystem, setCurrentSpeedProvider} from "./api.js"
|
import {currentSpeedProvider, getMovedDistanceFromToken, getRangesFromSpeedProvider, getUnreachableColorFromSpeedProvider, initApi, registerModule, registerSystem} from "./api.js"
|
||||||
import {getHexSizeSupportTokenGridCenter} from "./compatibility.js"
|
import {checkDependencies, getHexSizeSupportTokenGridCenter} from "./compatibility.js";
|
||||||
import {measure, moveTokens, onMouseMove} from "./foundry_imports.js"
|
import {moveTokens, onMouseMove} from "./foundry_imports.js"
|
||||||
|
import {performMigrations} from "./migration.js"
|
||||||
|
import {DragRulerRuler} from "./ruler.js";
|
||||||
|
import {getMovementHistory, resetMovementHistory} from "./movement_tracking.js";
|
||||||
import {registerSettings, settingsKey} from "./settings.js"
|
import {registerSettings, settingsKey} from "./settings.js"
|
||||||
|
import {SpeedProvider} from "./speed_provider.js"
|
||||||
|
|
||||||
Hooks.once("init", () => {
|
Hooks.once("init", () => {
|
||||||
registerSettings()
|
registerSettings()
|
||||||
|
initApi()
|
||||||
hookTokenDragHandlers()
|
hookTokenDragHandlers()
|
||||||
hookRulerFunctions()
|
|
||||||
hookKeyboardManagerFunctions()
|
hookKeyboardManagerFunctions()
|
||||||
patchRulerHighlightMeasurement()
|
|
||||||
|
|
||||||
availableSpeedProviders["native"] = nativeSpeedProvider
|
Ruler = DragRulerRuler;
|
||||||
setCurrentSpeedProvider(nativeSpeedProvider)
|
|
||||||
|
|
||||||
window.dragRuler = {
|
window.dragRuler = {
|
||||||
getColorForDistance,
|
getColorForDistance,
|
||||||
|
getMovedDistanceFromToken,
|
||||||
registerModule,
|
registerModule,
|
||||||
registerSystem
|
registerSystem,
|
||||||
|
resetMovementHistory,
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
Hooks.once("ready", () => {
|
Hooks.once("ready", () => {
|
||||||
Hooks.callAll("dragRuler.ready")
|
performMigrations()
|
||||||
|
checkDependencies();
|
||||||
|
Hooks.callAll("dragRuler.ready", SpeedProvider)
|
||||||
})
|
})
|
||||||
|
|
||||||
Hooks.on("canvasReady", () => {
|
Hooks.on("canvasReady", () => {
|
||||||
@@ -38,6 +43,15 @@ Hooks.on("canvasReady", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Hooks.on("getCombatTrackerEntryContext", function (html, menu) {
|
||||||
|
const entry = {
|
||||||
|
name: "drag-ruler.resetMovementHistory",
|
||||||
|
icon: '<i class="fas fa-undo-alt"></i>',
|
||||||
|
callback: li => resetMovementHistory(ui.combat.combat, li.data('combatant-id')),
|
||||||
|
};
|
||||||
|
menu.splice(1, 0, entry);
|
||||||
|
});
|
||||||
|
|
||||||
function hookTokenDragHandlers() {
|
function hookTokenDragHandlers() {
|
||||||
const originalDragLeftStartHandler = Token.prototype._onDragLeftStart
|
const originalDragLeftStartHandler = Token.prototype._onDragLeftStart
|
||||||
Token.prototype._onDragLeftStart = function(event) {
|
Token.prototype._onDragLeftStart = function(event) {
|
||||||
@@ -66,42 +80,6 @@ function hookTokenDragHandlers() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hookRulerFunctions() {
|
|
||||||
const originalMoveTokenHandler = Ruler.prototype.moveToken
|
|
||||||
Ruler.prototype.moveToken = function (event) {
|
|
||||||
const eventHandled = onRulerMoveToken.call(this, event)
|
|
||||||
if (!eventHandled)
|
|
||||||
return originalMoveTokenHandler.call(this, event)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalToJSON = Ruler.prototype.toJSON
|
|
||||||
Ruler.prototype.toJSON = function () {
|
|
||||||
const json = originalToJSON.call(this)
|
|
||||||
if (this.draggedToken)
|
|
||||||
json["draggedToken"] = this.draggedToken.data._id
|
|
||||||
return json
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalUpdate = Ruler.prototype.update
|
|
||||||
Ruler.prototype.update = function (data) {
|
|
||||||
if (data.draggedToken) {
|
|
||||||
this.draggedToken = canvas.tokens.get(data.draggedToken)
|
|
||||||
}
|
|
||||||
originalUpdate.call(this, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalMeasure = Ruler.prototype.measure
|
|
||||||
Ruler.prototype.measure = function (destination, options={}) {
|
|
||||||
if (this.isDragRuler) {
|
|
||||||
return measure.call(this, destination, options)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return originalMeasure.call(this, destination, options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hookKeyboardManagerFunctions() {
|
function hookKeyboardManagerFunctions() {
|
||||||
const originalHandleKeys = KeyboardManager.prototype._handleKeys
|
const originalHandleKeys = KeyboardManager.prototype._handleKeys
|
||||||
KeyboardManager.prototype._handleKeys = function (event, key, up) {
|
KeyboardManager.prototype._handleKeys = function (event, key, up) {
|
||||||
@@ -123,10 +101,11 @@ function handleKeys(event, key, up) {
|
|||||||
function onKeyX(up) {
|
function onKeyX(up) {
|
||||||
if (up)
|
if (up)
|
||||||
return false
|
return false
|
||||||
if (!canvas.controls.ruler.isDragRuler)
|
const ruler = canvas.controls.ruler;
|
||||||
|
if (!ruler.isDragRuler)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
deleteWaypoint()
|
ruler.dragRulerDeleteWaypoint();
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,17 +121,21 @@ function onKeyShift(up) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onTokenLeftDragStart(event) {
|
function onTokenLeftDragStart(event) {
|
||||||
|
if (!currentSpeedProvider.usesRuler(this))
|
||||||
|
return
|
||||||
const ruler = canvas.controls.ruler
|
const ruler = canvas.controls.ruler
|
||||||
ruler.draggedToken = this
|
ruler.draggedToken = this
|
||||||
let tokenCenter
|
let tokenCenter
|
||||||
if (canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(this))
|
if (canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(this))
|
||||||
tokenCenter = getHexSizeSupportTokenGridCenter(this)
|
tokenCenter = getHexSizeSupportTokenGridCenter(this)
|
||||||
else
|
else
|
||||||
tokenCenter = {x: this.x + canvas.grid.grid.w / 2, y: this.y + canvas.grid.grid.h / 2}
|
tokenCenter = this.center
|
||||||
ruler.clear();
|
ruler.clear();
|
||||||
ruler._state = Ruler.STATES.STARTING;
|
ruler._state = Ruler.STATES.STARTING;
|
||||||
ruler.rulerOffset = {x: tokenCenter.x - event.data.origin.x, y: tokenCenter.y - event.data.origin.y}
|
ruler.rulerOffset = {x: tokenCenter.x - event.data.origin.x, y: tokenCenter.y - event.data.origin.y}
|
||||||
addWaypoint.call(ruler, tokenCenter, false)
|
if (game.settings.get(settingsKey, "enableMovementHistory"))
|
||||||
|
ruler.dragRulerAddWaypointHistory(getMovementHistory(this));
|
||||||
|
ruler.dragRulerAddWaypoint(tokenCenter, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTokenLeftDragMove(event) {
|
function onTokenLeftDragMove(event) {
|
||||||
@@ -165,100 +148,37 @@ function onTokenDragLeftDrop(event) {
|
|||||||
const ruler = canvas.controls.ruler
|
const ruler = canvas.controls.ruler
|
||||||
if (!ruler.isDragRuler)
|
if (!ruler.isDragRuler)
|
||||||
return false
|
return false
|
||||||
const selectedTokens = canvas.tokens.placeables.filter(token => token._controlled)
|
onMouseMove.call(ruler, event);
|
||||||
|
const selectedTokens = canvas.tokens.controlled
|
||||||
|
ruler._state = Ruler.STATES.MOVING
|
||||||
moveTokens.call(ruler, ruler.draggedToken, selectedTokens)
|
moveTokens.call(ruler, ruler.draggedToken, selectedTokens)
|
||||||
ruler.draggedToken = null
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTokenDragLeftCancel(event) {
|
function onTokenDragLeftCancel(event) {
|
||||||
// This function is invoked by right clicking
|
// This function is invoked by right clicking
|
||||||
const ruler = canvas.controls.ruler
|
const ruler = canvas.controls.ruler
|
||||||
if (!ruler.isDragRuler)
|
if (!ruler.isDragRuler || ruler._state === Ruler.STATES.MOVING)
|
||||||
return false
|
return false
|
||||||
if (ruler._state === Ruler.STATES.MEASURING) {
|
if (ruler._state === Ruler.STATES.MEASURING) {
|
||||||
if (!game.settings.get(settingsKey, "swapSpacebarRightClick")) {
|
if (!game.settings.get(settingsKey, "swapSpacebarRightClick")) {
|
||||||
if (ruler.waypoints.length > 1)
|
ruler.dragRulerDeleteWaypoint(event);
|
||||||
event.preventDefault()
|
|
||||||
deleteWaypoint()
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const snap = !event.shiftKey
|
const snap = !event.shiftKey
|
||||||
addWaypoint.call(ruler, ruler.destination, snap)
|
ruler.dragRulerAddWaypoint(ruler.destination, snap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRulerMoveToken(event) {
|
|
||||||
// This function is invoked by left clicking
|
|
||||||
if (!this.isDragRuler)
|
|
||||||
return false
|
|
||||||
if (!game.settings.get(settingsKey, "swapSpacebarRightClick")) {
|
|
||||||
const snap = !event.shiftKey
|
|
||||||
addWaypoint.call(this, this.destination, snap)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
deleteWaypoint()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function addWaypoint(point, snap=true) {
|
|
||||||
if (snap)
|
|
||||||
point = canvas.grid.getCenter(point.x, point.y);
|
|
||||||
else
|
|
||||||
point = [point.x, point.y]
|
|
||||||
this.waypoints.push(new PIXI.Point(point[0], point[1]));
|
|
||||||
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteWaypoint() {
|
|
||||||
const ruler = canvas.controls.ruler
|
|
||||||
if (ruler.waypoints.length > 1) {
|
|
||||||
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens)
|
|
||||||
const rulerOffset = ruler.rulerOffset
|
|
||||||
ruler._removeWaypoint({x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y})
|
|
||||||
game.user.broadcastActivity({ruler: ruler})
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const token = ruler.draggedToken
|
|
||||||
ruler._endMeasurement()
|
|
||||||
ruler.draggedToken = null
|
|
||||||
|
|
||||||
// Deactivate the drag workflow in mouse
|
|
||||||
token.mouseInteractionManager._deactivateDragEvents();
|
|
||||||
token.mouseInteractionManager.state = token.mouseInteractionManager.states.HOVER;
|
|
||||||
|
|
||||||
// This will cancel the current drag operation
|
|
||||||
// Pass in a fake event that hopefully is enough to allow other modules to function
|
|
||||||
token._onDragLeftCancel({preventDefault: () => {return}})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function strInsertAfter(haystack, needle, strToInsert) {
|
|
||||||
const pos = haystack.indexOf(needle) + needle.length
|
|
||||||
return haystack.slice(0, pos) + strToInsert + haystack.slice(pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
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}]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getColorForDistance(startDistance, subDistance=0) {
|
export function getColorForDistance(startDistance, subDistance=0) {
|
||||||
if (!this.isDragRuler)
|
if (!this.isDragRuler)
|
||||||
return this.color
|
return this.color
|
||||||
|
if (!this.draggedToken.actor) {
|
||||||
|
return this.color;
|
||||||
|
}
|
||||||
// Don't apply colors if the current user doesn't have at least observer permissions
|
// Don't apply colors if the current user doesn't have at least observer permissions
|
||||||
if (this.draggedToken.actor.permission < 2) {
|
if (this.draggedToken.actor.permission < 2) {
|
||||||
// If this is a pc and alwaysShowSpeedForPCs is enabled we show the color anyway
|
// If this is a pc and alwaysShowSpeedForPCs is enabled we show the color anyway
|
||||||
@@ -266,33 +186,15 @@ export function getColorForDistance(startDistance, subDistance=0) {
|
|||||||
return this.color
|
return this.color
|
||||||
}
|
}
|
||||||
const distance = startDistance + subDistance
|
const distance = startDistance + subDistance
|
||||||
const firstColor = game.settings.get(settingsKey, "staticFirstColor") ? 0x00FF00 : this.color
|
if (!this.dragRulerRanges)
|
||||||
const ranges = currentSpeedProvider(this.draggedToken, firstColor)
|
this.dragRulerRanges = getRangesFromSpeedProvider(this.draggedToken)
|
||||||
|
const ranges = this.dragRulerRanges;
|
||||||
if (ranges.length === 0)
|
if (ranges.length === 0)
|
||||||
return this.color
|
return this.color
|
||||||
const currentRange = ranges.reduce((minRange, currentRange) => {
|
const currentRange = ranges.reduce((minRange, currentRange) => {
|
||||||
if (distance <= currentRange.range && currentRange.range < minRange.range)
|
if (distance <= currentRange.range && currentRange.range < minRange.range)
|
||||||
return currentRange
|
return currentRange
|
||||||
return minRange
|
return minRange
|
||||||
}, {range: Infinity, color: 0xFF0000})
|
}, {range: Infinity, color: getUnreachableColorFromSpeedProvider()})
|
||||||
return currentRange.color
|
return currentRange.color
|
||||||
}
|
}
|
||||||
|
|
||||||
// These patches were written with foundry-0.7.9.js as reference
|
|
||||||
function patchRulerHighlightMeasurement() {
|
|
||||||
let code = Ruler.prototype._highlightMeasurement.toString()
|
|
||||||
// Replace CRLF with LF in case foundry.js has CRLF for some reason
|
|
||||||
code = code.replace(/\r\n/g, "\n")
|
|
||||||
// Remove function signature and closing curly bracket (those are on the first and last line)
|
|
||||||
code = code.slice(code.indexOf("\n"), code.lastIndexOf("\n"))
|
|
||||||
|
|
||||||
const calcColorCode = `
|
|
||||||
let subDistance = canvas.grid.measureDistances([{ray: new Ray(ray.A, {x: xg, y: yg})}], {gridSpaces: true})[0]
|
|
||||||
let color = dragRuler.getColorForDistance.call(this, startDistance, subDistance)
|
|
||||||
`
|
|
||||||
|
|
||||||
code = strInsertAfter(code, "Position(x1, y1);\n", calcColorCode)
|
|
||||||
code = strInsertAfter(code, "Position(x1h, y1h);\n", calcColorCode.replace("x: xg, y: yg", "x: xgh, y: ygh"))
|
|
||||||
code = code.replace(/color: this\.color\}/g, "color}")
|
|
||||||
Ruler.prototype._highlightMeasurement = new Function("ray", "startDistance=undefined", code)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import {settingsKey} from "./settings.js"
|
||||||
|
|
||||||
|
const currentDataVersion = "1.3.0"
|
||||||
|
|
||||||
|
export function performMigrations() {
|
||||||
|
if (!game.user.isGM)
|
||||||
|
return
|
||||||
|
|
||||||
|
let dataVersion = game.settings.get(settingsKey, "dataVersion")
|
||||||
|
if (dataVersion === "fresh install") {
|
||||||
|
// Migration from unnamed version. TODO Remove this in a future version
|
||||||
|
let speedAttribute = game.settings.storage.get("world").get(`${settingsKey}.speedAttribute`)
|
||||||
|
if (speedAttribute)
|
||||||
|
speedAttribute = speedAttribute.slice(1, speedAttribute.length - 1)
|
||||||
|
const speedAttributeDefault = game.settings.get(settingsKey, "speedProviders.native.setting.speedAttribute")
|
||||||
|
if (speedAttribute !== speedAttributeDefault)
|
||||||
|
game.settings.set(settingsKey, "speedProviders.native.setting.speedAttribute", speedAttribute)
|
||||||
|
|
||||||
|
let dashMultiplier = game.settings.storage.get("world").get(`${settingsKey}.dashMultiplier`)
|
||||||
|
const dashMultiplierDefault = game.settings.get(settingsKey, "speedProviders.native.setting.dashMultiplier")
|
||||||
|
if (dashMultiplier !== dashMultiplierDefault)
|
||||||
|
game.settings.set(settingsKey, "speedProviders.native.setting.dashMultiplier", dashMultiplier)
|
||||||
|
// End of unnamed version migration code
|
||||||
|
game.settings.set(settingsKey, "dataVersion", currentDataVersion)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import {measureDistances} from "./compatibility.js";
|
||||||
|
import {updateCombatantDragRulerFlags} from "./socket.js";
|
||||||
|
import {getTokenShape, zip} from "./util.js";
|
||||||
|
|
||||||
|
function initTrackingFlag(combatant) {
|
||||||
|
const initialFlag = {passedWaypoints: [], trackedRound: 0};
|
||||||
|
let dragRulerFlag = combatant.flags?.dragRuler;
|
||||||
|
if (dragRulerFlag) {
|
||||||
|
if (isNaN(dragRulerFlag.trackedRound)) {
|
||||||
|
mergeObject(dragRulerFlag, initialFlag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
combatant.flags.dragRuler = initialFlag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitializedCombatant(token, combat) {
|
||||||
|
const combatant = combat.getCombatantByToken(token.data._id);
|
||||||
|
if (!combatant)
|
||||||
|
return undefined;
|
||||||
|
initTrackingFlag(combatant);
|
||||||
|
return combatant;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function trackRays(tokens, tokenRays) {
|
||||||
|
const combat = game.combat;
|
||||||
|
if (!combat)
|
||||||
|
return;
|
||||||
|
if (!combat.started)
|
||||||
|
return;
|
||||||
|
if (!(tokens instanceof Array)) {
|
||||||
|
tokens = [tokens];
|
||||||
|
tokenRays = [tokenRays];
|
||||||
|
}
|
||||||
|
const updates = Array.from(zip(tokens, tokenRays)).map(([token, rays]) => calculateUpdate(combat, token, rays)).filter(Boolean);
|
||||||
|
await updateCombatantDragRulerFlags(combat, updates);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateUpdate(combat, token, rays) {
|
||||||
|
const combatant = getInitializedCombatant(token, combat);
|
||||||
|
if (!combatant)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Check if we have entered a new round. If so, remove the currently stored path
|
||||||
|
if (combat.data.round > combatant.flags.dragRuler.trackedRound) {
|
||||||
|
combatant.flags.dragRuler.passedWaypoints = [];
|
||||||
|
combatant.flags.dragRuler.trackedRound = combat.data.round;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the passed waypoints to the combatant
|
||||||
|
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS;
|
||||||
|
const dragRulerFlags = combatant.flags.dragRuler;
|
||||||
|
const waypoints = dragRulerFlags.passedWaypoints;
|
||||||
|
for (const ray of rays) {
|
||||||
|
// Ignore rays that have the same start and end coordinates
|
||||||
|
if (ray.A.x !== ray.B.x || ray.A.y !== ray.B.y) {
|
||||||
|
if (terrainRulerAvailable) {
|
||||||
|
measureDistances([{ray}], token, getTokenShape(token), true, {terrainRulerInitialState: dragRulerFlags.rulerState});
|
||||||
|
ray.A.dragRulerVisitedSpaces = ray.terrainRulerVisitedSpaces;
|
||||||
|
dragRulerFlags.rulerState = ray.terrainRulerFinalState;
|
||||||
|
}
|
||||||
|
waypoints.push(ray.A);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {_id: combatant._id, dragRulerFlags};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMovementHistory(token) {
|
||||||
|
const combat = game.combat;
|
||||||
|
if (!combat)
|
||||||
|
return [];
|
||||||
|
const combatant = combat.getCombatantByToken(token.data._id);
|
||||||
|
if (!combatant)
|
||||||
|
return [];
|
||||||
|
const dragRulerFlags = combatant.flags.dragRuler;
|
||||||
|
if (!dragRulerFlags)
|
||||||
|
return [];
|
||||||
|
if (combat.data.round > dragRulerFlags.trackedRound)
|
||||||
|
return [];
|
||||||
|
return dragRulerFlags.passedWaypoints ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resetMovementHistory(combat, combatantId) {
|
||||||
|
const combatant = combat.getCombatant(combatantId);
|
||||||
|
const dragRulerFlags = combatant.flags.dragRuler;
|
||||||
|
if (!dragRulerFlags)
|
||||||
|
return;
|
||||||
|
dragRulerFlags.passedWaypoints = null;
|
||||||
|
dragRulerFlags.trackedRound = null;
|
||||||
|
dragRulerFlags.rulerState = null;
|
||||||
|
await updateCombatantDragRulerFlags(combat, [{_id: combatantId, dragRulerFlags}]);
|
||||||
|
}
|
||||||
+112
@@ -0,0 +1,112 @@
|
|||||||
|
import {measure} from "./foundry_imports.js"
|
||||||
|
import {settingsKey} from "./settings.js";
|
||||||
|
import {getSnapPointForToken} from "./util.js";
|
||||||
|
|
||||||
|
export class DragRulerRuler extends Ruler {
|
||||||
|
// Functions below are overridden versions of functions in Ruler
|
||||||
|
constructor(user, {color=null}={}) {
|
||||||
|
super(user, {color});
|
||||||
|
this.previousWaypoints = [];
|
||||||
|
this.previousLabels = this.addChild(new PIXI.Container());
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
super.clear();
|
||||||
|
this.previousWaypoints = [];
|
||||||
|
this.previousLabels.removeChildren().forEach(c => c.destroy());
|
||||||
|
this.dragRulerRanges = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveToken(event) {
|
||||||
|
// This function is invoked by left clicking
|
||||||
|
if (!this.isDragRuler)
|
||||||
|
return await super.moveToken(event);
|
||||||
|
if (!game.settings.get(settingsKey, "swapSpacebarRightClick")) {
|
||||||
|
const snap = !event.shiftKey;
|
||||||
|
this.dragRulerAddWaypoint(this.destination, snap);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.dragRulerDeleteWaypoint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
const json = super.toJSON();
|
||||||
|
if (this.draggedToken)
|
||||||
|
json["draggedToken"] = this.draggedToken.data._id;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(data) {
|
||||||
|
// Don't show a GMs drag ruler to non GM players
|
||||||
|
if (data.draggedToken && this.user.isGM && !game.user.isGM && !game.settings.get(settingsKey, "showGMRulerToPlayers"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (data.draggedToken) {
|
||||||
|
this.draggedToken = canvas.tokens.get(data.draggedToken);
|
||||||
|
}
|
||||||
|
super.update(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
measure(destination, options={}) {
|
||||||
|
if (this.isDragRuler) {
|
||||||
|
return measure.call(this, destination, options);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return super.measure(destination, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_endMeasurement() {
|
||||||
|
super._endMeasurement();
|
||||||
|
this.draggedToken = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The functions below aren't present in the orignal Ruler class and are added by Drag Ruler
|
||||||
|
dragRulerAddWaypoint(point, snap=true) {
|
||||||
|
if (snap)
|
||||||
|
point = getSnapPointForToken(point.x, point.y, this.draggedToken);
|
||||||
|
this.waypoints.push(new PIXI.Point(point.x, point.y));
|
||||||
|
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
|
||||||
|
}
|
||||||
|
|
||||||
|
dragRulerAddWaypointHistory(waypoints) {
|
||||||
|
waypoints.forEach(waypoint => waypoint.isPrevious = true);
|
||||||
|
this.waypoints = this.waypoints.concat(waypoints);
|
||||||
|
for (const waypoint of waypoints) {
|
||||||
|
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dragRulerDeleteWaypoint(event={preventDefault: () => {return}}) {
|
||||||
|
if (this.waypoints.filter(w => !w.isPrevious).length > 1) {
|
||||||
|
event.preventDefault();
|
||||||
|
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens);
|
||||||
|
const rulerOffset = this.rulerOffset;
|
||||||
|
this._removeWaypoint({x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y});
|
||||||
|
game.user.broadcastActivity({ruler: this});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const token = this.draggedToken;
|
||||||
|
this._endMeasurement();
|
||||||
|
|
||||||
|
// Deactivate the drag workflow in mouse
|
||||||
|
token.mouseInteractionManager._deactivateDragEvents();
|
||||||
|
token.mouseInteractionManager.state = token.mouseInteractionManager.states.HOVER;
|
||||||
|
|
||||||
|
// This will cancel the current drag operation
|
||||||
|
// Pass in a fake event that hopefully is enough to allow other modules to function
|
||||||
|
token._onDragLeftCancel(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+222
-37
@@ -1,9 +1,16 @@
|
|||||||
import {updateSpeedProvider} from "./api.js";
|
import {availableSpeedProviders, currentSpeedProvider, getDefaultSpeedProvider, updateSpeedProvider} from "./api.js";
|
||||||
import {getDefaultDashMultiplier, getDefaultSpeedAttribute} from "./systems.js"
|
import {SpeedProvider} from "./speed_provider.js"
|
||||||
|
|
||||||
export const settingsKey = "drag-ruler";
|
export const settingsKey = "drag-ruler";
|
||||||
|
|
||||||
export function registerSettings() {
|
export function registerSettings() {
|
||||||
|
game.settings.register(settingsKey, "dataVersion", {
|
||||||
|
scope: "world",
|
||||||
|
config: false,
|
||||||
|
type: String,
|
||||||
|
default: "fresh install"
|
||||||
|
})
|
||||||
|
|
||||||
game.settings.register(settingsKey, "swapSpacebarRightClick", {
|
game.settings.register(settingsKey, "swapSpacebarRightClick", {
|
||||||
name: "drag-ruler.settings.swapSpacebarRightClick.name",
|
name: "drag-ruler.settings.swapSpacebarRightClick.name",
|
||||||
hint: "drag-ruler.settings.swapSpacebarRightClick.hint",
|
hint: "drag-ruler.settings.swapSpacebarRightClick.hint",
|
||||||
@@ -22,44 +29,222 @@ export function registerSettings() {
|
|||||||
default: true,
|
default: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
// This setting will be modified by the api if modules register to it
|
game.settings.register(settingsKey, "showGMRulerToPlayers", {
|
||||||
game.settings.register(settingsKey, "speedProvider", {
|
name: "drag-ruler.settings.showGMRulerToPlayers.name",
|
||||||
name: "drag-ruler.settings.speedProvider.name",
|
hint: "drag-ruler.settings.showGMRulerToPlayers.hint",
|
||||||
hint: "drag-ruler.settings.speedProvider.hint",
|
|
||||||
scope: "world",
|
|
||||||
config: false,
|
|
||||||
type: Object,
|
|
||||||
choices: {
|
|
||||||
"native": game.i18n.localize("drag-ruler.settings.speedProvider.choices.native")
|
|
||||||
},
|
|
||||||
default: "native",
|
|
||||||
onChange: updateSpeedProvider,
|
|
||||||
})
|
|
||||||
|
|
||||||
game.settings.register(settingsKey, "speedAttribute", {
|
|
||||||
name: "drag-ruler.settings.speedAttribute.name",
|
|
||||||
hint: "drag-ruler.settings.speedAttribute.hint",
|
|
||||||
scope: "world",
|
|
||||||
config: true,
|
|
||||||
type: String,
|
|
||||||
default: getDefaultSpeedAttribute(),
|
|
||||||
})
|
|
||||||
|
|
||||||
game.settings.register(settingsKey, "dashMultiplier", {
|
|
||||||
name: "drag-ruler.settings.dashMultiplier.name",
|
|
||||||
hint: "drag-ruler.settings.dashMultiplier.hint",
|
|
||||||
scope: "world",
|
|
||||||
config: true,
|
|
||||||
type: Number,
|
|
||||||
default: getDefaultDashMultiplier(),
|
|
||||||
})
|
|
||||||
|
|
||||||
game.settings.register(settingsKey, "staticFirstColor", {
|
|
||||||
name: "drag-ruler.settings.staticFirstColor.name",
|
|
||||||
hint: "drag-ruler.settings.staticFirstColor.hint",
|
|
||||||
scope: "world",
|
scope: "world",
|
||||||
config: true,
|
config: true,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
game.settings.register(settingsKey, "enableMovementHistory", {
|
||||||
|
name: "drag-ruler.settings.enableMovementHistory.name",
|
||||||
|
hint: "drag-ruler.settings.enableMovementHistory.hint",
|
||||||
|
scope: "world",
|
||||||
|
config: true,
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
game.settings.register(settingsKey, "lastTerrainRulerHintTime", {
|
||||||
|
config: false,
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
game.settings.register(settingsKey, "neverShowTerrainRulerHint", {
|
||||||
|
config: false,
|
||||||
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// This setting will be modified by the api if modules register to it
|
||||||
|
game.settings.register(settingsKey, "speedProvider", {
|
||||||
|
scope: "world",
|
||||||
|
config: false,
|
||||||
|
type: String,
|
||||||
|
default: getDefaultSpeedProvider(),
|
||||||
|
onChange: updateSpeedProvider,
|
||||||
|
})
|
||||||
|
|
||||||
|
game.settings.registerMenu(settingsKey, "speedProviderSettings", {
|
||||||
|
name: "drag-ruler.settings.speedProviderSettings.name",
|
||||||
|
hint: "drag-ruler.settings.speedProviderSettings.hint",
|
||||||
|
label: "drag-ruler.settings.speedProviderSettings.button",
|
||||||
|
icon: "fas fa-tachometer-alt",
|
||||||
|
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,
|
||||||
|
choices: data.providers.reduce((choices, provider) => {
|
||||||
|
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
|
||||||
|
const settings = []
|
||||||
|
for (const setting of provider.settings) {
|
||||||
|
try {
|
||||||
|
if (setting.scope === "world" && !game.user.isGM)
|
||||||
|
continue
|
||||||
|
const s = duplicate(setting)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import {currentSpeedProvider} from "./api.js";
|
||||||
|
|
||||||
|
let socket;
|
||||||
|
|
||||||
|
Hooks.once("socketlib.ready", () => {
|
||||||
|
socket = socketlib.registerModule("drag-ruler");
|
||||||
|
socket.register("updateCombatantDragRulerFlags", _socketUpdateCombatantDragRulerFlags);
|
||||||
|
});
|
||||||
|
|
||||||
|
export function updateCombatantDragRulerFlags(combat, updates) {
|
||||||
|
const combatId = combat.id;
|
||||||
|
// TODO Check if canvas.tokens.get is still neccessary in future foundry versions
|
||||||
|
return socket.executeAsGM(_socketUpdateCombatantDragRulerFlags, combatId, updates)
|
||||||
|
.then(() => currentSpeedProvider.onMovementHistoryUpdate(updates.map(update => canvas.tokens.get(combat.getCombatant(update._id).token._id))));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _socketUpdateCombatantDragRulerFlags(combatId, updates) {
|
||||||
|
const user = game.users.get(this.socketdata.userId);
|
||||||
|
const combat = game.combats.get(combatId);
|
||||||
|
const requestedUpdates = updates.length;
|
||||||
|
updates = updates.filter(update => {
|
||||||
|
const actor = combat.getCombatant(update._id).actor;
|
||||||
|
if (!actor)
|
||||||
|
return false;
|
||||||
|
return actor.hasPerm(user, "OWNER");
|
||||||
|
});
|
||||||
|
if (updates.length !== requestedUpdates) {
|
||||||
|
console.warn(`Some of the movement history updates requested by user '${game.users.get(this.socketdata.userId).name}' were not performed because the user lacks owner permissions for those tokens`);
|
||||||
|
}
|
||||||
|
updates = updates.map(update => {
|
||||||
|
return {_id: update._id, flags: {dragRuler: update.dragRulerFlags}};
|
||||||
|
});
|
||||||
|
await combat.updateEmbeddedEntity("Combatant", updates, {diff: false});
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
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, ...)
|
||||||
|
* This function is only called if the TerrainLayer and TerrainRuler modules are enabled.
|
||||||
|
*
|
||||||
|
* Implementing this method is optional and only needs to be done if you want to provide a custom cost function (for example to allow tokens to ignore difficult terrain)
|
||||||
|
*/
|
||||||
|
getCostForStep(token, area) {
|
||||||
|
// Lookup the cost for each square occupied by the token
|
||||||
|
const costs = area.map(space => terrainRuler.getCost(space.x, space.y, {token}));
|
||||||
|
// Return the maximum of the costs
|
||||||
|
return costs.reduce((max, current) => Math.max(max, current))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a boolean indicating whether this token will use a Ruler or not.
|
||||||
|
* If this is returns `false` for a token Drag Ruler will be disabled for that token. Dragging a token for which this function
|
||||||
|
* 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 = parseInt(getProperty(token, speedAttribute));
|
||||||
|
if (tokenSpeed === undefined) {
|
||||||
|
console.warn(`Drag Ruler (Generic Speed Provider) | The configured token speed attribute "${speedAttribute}" didn't return a speed value. To use colors based on drag distance set the setting to the correct value (or clear the box to disable this feature).`)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
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,17 +1,27 @@
|
|||||||
|
|
||||||
export function getDefaultSpeedAttribute() {
|
export function getDefaultSpeedAttribute() {
|
||||||
switch (game.system.id) {
|
switch (game.system.id) {
|
||||||
|
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.mech.speed"
|
||||||
case "pf1":
|
case "pf1":
|
||||||
return "actor.data.data.attributes.speed.land.total"
|
return "actor.data.data.attributes.speed.land.total"
|
||||||
|
case "swade":
|
||||||
|
return "actor.data.data.stats.speed.value"
|
||||||
}
|
}
|
||||||
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":
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|||||||
+169
@@ -0,0 +1,169 @@
|
|||||||
|
import {getPixelsFromGridPosition} from "./foundry_fixes.js"
|
||||||
|
|
||||||
|
export function* zip(it1, it2) {
|
||||||
|
for (let i = 0;i < Math.min(it1.length, it2.length);i++) {
|
||||||
|
yield [it1[i], it2[i]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSnapPointForToken(x, y, token) {
|
||||||
|
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||||
|
return new PIXI.Point(x, y);
|
||||||
|
}
|
||||||
|
if (canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) {
|
||||||
|
if (token.getFlag("hex-size-support", "borderSize") % 2 === 0) {
|
||||||
|
const snapPoint = CONFIG.hexSizeSupport.findVertexSnapPoint(x, y, token, canvas.grid.grid)
|
||||||
|
return new PIXI.Point(snapPoint.x, snapPoint.y)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return new PIXI.Point(...canvas.grid.getCenter(x, y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Tiny tokens can snap to the cells corners
|
||||||
|
if (!canvas.grid.isHex && token.data.width <= 0.5) {
|
||||||
|
const [topLeftX, topLeftY] = canvas.grid.getTopLeft(x, y);
|
||||||
|
const offsetX = x - topLeftX;
|
||||||
|
const offsetY = y - topLeftY;
|
||||||
|
const subGridWidth = Math.floor(canvas.grid.w / 2);
|
||||||
|
const subGridHeight = Math.floor(canvas.grid.h / 2);
|
||||||
|
const subGridPosX = Math.floor(offsetX / subGridWidth);
|
||||||
|
const subGridPosY = Math.floor(offsetY / subGridHeight);
|
||||||
|
return new PIXI.Point(topLeftX + (subGridPosX + 0.5) * subGridWidth, topLeftY + (subGridPosY + 0.5) * subGridHeight);
|
||||||
|
}
|
||||||
|
// Hex tokens, tokens with odd multipliers (1x1, 3x3, ...) and tokens smaller than 1x1 but bigger than 0.5x0.5 snap to the center of the grid cell
|
||||||
|
if (canvas.grid.isHex || Math.round(token.data.width) % 2 === 1 || token.data.width < 1) {
|
||||||
|
return new PIXI.Point(...canvas.grid.getCenter(x, y))
|
||||||
|
}
|
||||||
|
// All remaining tokens (those with even or fractional multipliers on square grids) snap to the intersection points of the grid
|
||||||
|
const [snappedX, snappedY] = canvas.grid.getCenter(x - canvas.grid.w / 2, y - canvas.grid.h / 2)
|
||||||
|
return new PIXI.Point(snappedX + canvas.grid.w / 2, snappedY + canvas.grid.h / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function highlightTokenShape(position, shape, color, alpha) {
|
||||||
|
const layer = canvas.grid.highlightLayers[this.name];
|
||||||
|
if ( !layer )
|
||||||
|
return false;
|
||||||
|
const area = getAreaFromPositionAndShape(position, shape);
|
||||||
|
for (const space of area) {
|
||||||
|
const [x, y] = getPixelsFromGridPosition(space.x, space.y);
|
||||||
|
canvas.grid.grid.highlightGridPosition(layer, {x, y, color, alpha: 0.25 * alpha});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAreaFromPositionAndShape(position, shape) {
|
||||||
|
return shape.map(space => {
|
||||||
|
let x = position.x + space.x;
|
||||||
|
let y = position.y + space.y;
|
||||||
|
if (canvas.grid.isHex) {
|
||||||
|
let shiftedRow;
|
||||||
|
if (canvas.grid.grid.options.even)
|
||||||
|
shiftedRow = 1
|
||||||
|
else
|
||||||
|
shiftedRow = 0
|
||||||
|
if (canvas.grid.grid.options.columns) {
|
||||||
|
if (space.x % 2 !== 0 && position.x % 2 !== shiftedRow) {
|
||||||
|
y += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (space.y % 2 !== 0 && position.y % 2 !== shiftedRow) {
|
||||||
|
x += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {x, y}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTokenShape(token) {
|
||||||
|
if (token.scene.data.gridType === CONST.GRID_TYPES.GRIDLESS) {
|
||||||
|
return [{x: 0, y: 0}]
|
||||||
|
}
|
||||||
|
else if (token.scene.data.gridType === CONST.GRID_TYPES.SQUARE) {
|
||||||
|
const topOffset = -Math.floor(token.data.height / 2)
|
||||||
|
const leftOffset = -Math.floor(token.data.width / 2)
|
||||||
|
const shape = []
|
||||||
|
for (let y = 0;y < token.data.height;y++) {
|
||||||
|
for (let x = 0;x < token.data.width;x++) {
|
||||||
|
shape.push({x: x + leftOffset, y: y + topOffset})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shape
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Hex grids
|
||||||
|
if (game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) {
|
||||||
|
const borderSize = token.data.flags["hex-size-support"].borderSize;
|
||||||
|
let shape = [{x: 0, y: 0}];
|
||||||
|
if (borderSize >= 2)
|
||||||
|
shape = shape.concat([{x: 0, y: -1}, {x: -1, y: -1}]);
|
||||||
|
if (borderSize >= 3)
|
||||||
|
shape = shape.concat([{x: 0, y: 1}, {x: -1, y: 1}, {x: -1, y: 0}, {x: 1, y: 0}]);
|
||||||
|
if (borderSize >= 4)
|
||||||
|
shape = shape.concat([{x: -2, y: -1}, {x: 1, y: -1}, {x: -1, y: -2}, {x: 0, y: -2}, {x: 1, y: -2}])
|
||||||
|
|
||||||
|
if (Boolean(CONFIG.hexSizeSupport.getAltOrientationFlag(token)) !== canvas.grid.grid.options.columns)
|
||||||
|
shape.forEach(space => space.y *= -1);
|
||||||
|
if (canvas.grid.grid.options.columns)
|
||||||
|
shape = shape.map(space => {return {x: space.y, y: space.x}});
|
||||||
|
return shape;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return [{x: 0, y: 0}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTokenSize(token) {
|
||||||
|
let w, h;
|
||||||
|
const hexSizeSupportBorderSize = token.data.flags["hex-size-support"]?.borderSize;
|
||||||
|
if (hexSizeSupportBorderSize > 0) {
|
||||||
|
w = h = hexSizeSupportBorderSize
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
w = token.data.width
|
||||||
|
h = token.data.height
|
||||||
|
}
|
||||||
|
return {w, h};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tokens that have a size divisible by two (2x2, 4x4, 2x1) have their ruler at the edge of a cell.
|
||||||
|
// This function applies an offset to to the waypoints that will move the ruler from the edge to the center of the cell
|
||||||
|
export function applyTokenSizeOffset(waypoints, token) {
|
||||||
|
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||||
|
return waypoints
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenSize = getTokenSize(token);
|
||||||
|
const waypointOffset = {x: 0, y: 0};
|
||||||
|
if (canvas.grid.isHex) {
|
||||||
|
if (game.modules.get("hex-size-support")?.active) {
|
||||||
|
const isAltOrientation = CONFIG.hexSizeSupport.getAltOrientationFlag(token);
|
||||||
|
if (canvas.grid.grid.options.columns) {
|
||||||
|
if (tokenSize.w % 2 === 0) {
|
||||||
|
waypointOffset.x = canvas.grid.w / 2;
|
||||||
|
if (!isAltOrientation)
|
||||||
|
waypointOffset.x *= -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (tokenSize.h % 2 === 0) {
|
||||||
|
waypointOffset.y = canvas.grid.h / 2;
|
||||||
|
if (isAltOrientation)
|
||||||
|
waypointOffset.y *= -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If hex size support isn't active leave the waypoints like they are
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (tokenSize.w % 2 === 0) {
|
||||||
|
waypointOffset.x = canvas.grid.w / 2;
|
||||||
|
}
|
||||||
|
if (tokenSize.h % 2 === 0) {
|
||||||
|
waypointOffset.y = canvas.grid.h / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return waypoints.map(w => new PIXI.Point(w.x + waypointOffset.x, w.y + waypointOffset.y))
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
{{! 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>
|
||||||
Reference in New Issue
Block a user