Compare commits

..

47 Commits

Author SHA1 Message Date
Manuel Vögele aabe9c75e8 Release v1.3.0 2021-02-21 12:48:26 +01:00
Manuel Vögele 0fb8aecfdd Refer to additional API capabilities in the readme 2021-02-21 12:41:12 +01:00
Manuel Vögele bb5fe94439 Allow speed providers to disable Drag Ruler on a per-token basis (resolves #24) 2021-02-21 12:13:14 +01:00
Manuel Vögele 6f6655009d Update readme to match the new api 2021-02-21 11:45:13 +01:00
Manuel Vögele 8110a1b78b Add setting migration 2021-02-18 00:38:55 +01:00
Manuel Vögele b84cdfd7c1 Fix a bug that prevented the Speed Provider Settings Dialog from being shown if an unavailable Speed Provider is configured 2021-02-18 00:13:15 +01:00
Manuel Vögele 75d59171d6 New API, allwing color configuration (resolves #7) and Speed Provider Settings 2021-02-17 16:25:17 +01:00
Manuel Vögele 6ea03579b9 Add Cipher System to the list of Systems with Drag Ruler integration 2021-02-16 18:03:47 +01:00
Manuel Vögele fd5b439ec2 Use foundries builin function to fetch selected tokens 2021-02-16 16:04:47 +01:00
Manuel Vögele 52118a6142 Mention b29a687 (defaults for lancer system) in changelog 2021-02-11 18:38:34 +01:00
Grygon b29a687fff Add Lancer to systems.js (#21) 2021-02-11 18:36:46 +01:00
Manuel Vögele d232335e90 Release v1.2.2 2021-02-11 16:08:46 +01:00
Manuel Vögele b309464c76 Add credit for japanese translation 2021-02-11 16:06:46 +01:00
Brother Sharp 9ce3cc5eb8 Add japanese translation (#19) 2021-02-11 16:03:38 +01:00
Manuel Vögele 10346a7dbc Release v1.2.1 2021-02-11 09:11:27 +01:00
Manuel Vögele 6adb69829e Make compatible with Hex Token Size Support (resolves #12) 2021-02-11 09:06:54 +01:00
Manuel Vögele 94f5a49263 Cache canvas.controls.ruler into local variable for readability and performance 2021-02-11 00:27:11 +01:00
Manuel Vögele 3141deb3a2 Release v1.2.0 2021-02-08 19:04:19 +01:00
Manuel Vögele 2144a536ee Immediately show snapped/unsnapped measuring result when pressing/releasing shift 2021-02-08 19:02:09 +01:00
Manuel Vögele 2b66b43c55 Allow deleting of waypoints with the 'X' key (resolves #14) 2021-02-08 18:30:33 +01:00
Manuel Vögele 4bd1473310 Change ruler color on gridless maps to indecate different speeds (resolves #15) 2021-02-08 18:08:52 +01:00
Manuel Vögele 24620bd604 Add list of game systems with integration to the readme 2021-02-08 16:52:22 +01:00
Manuel Vögele 82685a1a2f Add setting to use a static color for the first speed instead of player color (resolves #10) 2021-02-08 16:27:11 +01:00
Manuel Vögele c09a85b470 Reword the changelog 2021-02-08 16:25:26 +01:00
Manuel Vögele 7d01ad6d33 Add b671928a to changelog 2021-02-08 15:48:10 +01:00
Manuel Vögele b671928ade Disable grid snapping while pressing shift (fixes #9) 2021-02-08 15:44:58 +01:00
Manuel Vögele 3d68e324f2 Apply ruler offset when determining destination after deleting a waypoint 2021-02-08 11:36:18 +01:00
Manuel Vögele c30ff10364 Remove defective un-snapping code 2021-02-08 11:27:37 +01:00
Manuel Vögele 8b69cb2f65 Allow swapping of right click and spacebar behavior (resolves #4) 2021-02-08 11:24:48 +01:00
Manuel Vögele 0647fec08f Import the wohle measure function instead of patching it (preparation for future changes) 2021-02-07 12:26:08 +01:00
Manuel Vögele ebde56513d Release v1.1.1 2021-02-05 11:50:35 +01:00
Manuel Vögele 74c7d74c5a On gridless maps, always start measuring from the tokens center 2021-02-05 11:46:16 +01:00
Manuel Vögele f1542b7789 Start measuring immediately when the token is being dragged 2021-02-05 11:20:41 +01:00
Manuel Vögele 46edfa8ae6 Fix a bug where tokens wouldn't be moved to the corect end position on gridless maps 2021-02-05 00:54:09 +01:00
Manuel Vögele fe89a871c9 Release v1.1.0 2021-02-03 09:12:21 +01:00
Manuel Vögele 62c965b499 Clarify changelog 2021-02-03 08:27:58 +01:00
Manuel Vögele 4149f0e351 Fix #5: Handle installations where foundry.js has CRLF line endings gracefully 2021-02-03 00:52:59 +01:00
Manuel Vögele 5c43651925 Add option to show colors of PCs to anyone, regardless of permissions 2021-02-02 14:37:07 +01:00
Manuel Vögele 59f2e67717 Add ko-fi button to readme 2021-02-02 14:23:17 +01:00
Manuel Vögele 33cb9a2ba2 Only show range colors to players who have observer permissions for that token 2021-02-02 11:44:20 +01:00
Manuel Vögele c8a7352d5f Merge branch 'master' into develop 2021-02-02 11:04:18 +01:00
Manuel Vögele bbd2173e63 Release v1.0.1 2021-02-02 10:53:23 +01:00
Manuel Vögele 633563b672 Add custom implementation of Ruler.moveToken
This allows us to move multiple tokens at a time
and allows the gm to move tokens through walls again
2021-02-02 10:52:15 +01:00
Manuel Vögele 135a3091b0 Don't show the drag ruler to players who cannot se the dragged token
As a side effect this will now also show the coloring to other players
2021-02-01 21:42:38 +01:00
Manuel Vögele 6beae157e7 Fix typos in the example source code in the api documentation 2021-02-01 16:21:45 +01:00
Manuel Vögele dd5f9c4b24 Release v1.0.0 2021-02-01 15:39:38 +01:00
Manuel Vögele d189e8c770 Add README.md 2021-02-01 15:38:52 +01:00
23 changed files with 1169 additions and 121 deletions
+56
View File
@@ -0,0 +1,56 @@
## 1.3.0
### New features
- The color used to indicate speed ranges is now configurable
- The settings dialog has been reworked
### System compatibility
- Drag Ruler's Generic SpeedProvider is now aware of good default values for the lancer game system (thanks to Grygon)
### API changes
This release introduces a new API that is incompatible with the old API. The new API offers more flexibility for users and Speed Providers alike and allows to add new features in the future without breaking compatibility again. The old API will continue to function, but to profit from any of the features below Speed Providers need to switch to the new API. For more details check out the API documentation.
The following things have changed with the new API:
- Colors used by speed providers can now be changed by the user via configuration
- Speed Providers can now offer settings to the user that will be integrated into Drag Ruler's settings menu
- Speed Providers can now conditionally disable Drag Ruler for some tokens
## 1.2.2
### Translation
- Added japanese translation (thanks to touge)
## 1.2.1
### Compatiblity
- Drag Ruler is now compatible with Hex Token Size Support. For compatibility Hex Token Size Support Version 0.5.4 or higher is required. Thanks to Ourobor for helping making this possible.
## 1.2.0
### New features
- Right click and spacebar can now be swapped, allowing to place waypoints with right click and removing them with spacebar
- The module can now be configured use a fixed color instead of the player color for the first speed range
- On gridless maps the ruler will now change it's color to indicate the different speed ranges
- As an alternative to right click (or spacebar, if you have swapped right and spacebar behavior) waypoints can now also be deleted with the `X` key
### Bugfixes
- Disabling snap to grid with shift now works as expected
- Fixed a bug where the ruler would sometimes jump to a different target location when deleting a waypoint
## 1.1.1
### Bugfixes
- Fixed a bug where tokens wouldn't be moved to the corect end position on gridless maps
- Ruler now appears immediately when the token is being dragged
- On gridless maps the ruler will always start measuring at the center of the token
- This change has no impact on the distance that is being measured
- In addition to the cosmetical aspect this also fixes a bug that allowed players to glitch through walls
## 1.1.0
### New features
- The drag ruler will now be colored for other players than the dragging player as well (only if they have at least observer permissions for that token)
- The drag ruler won't be shown to other players if they cannot see the dragged token
### Bugfixes
- Fixed a bug where Drag Ruler wouldn't work at all on some windows installations (specificially where the `foundry.js` has `CRLF` line endings)
## 1.0.1
### Bugfixes
- The GM can now move tokens through walls
- It is now possible to move multiple tokens with Drag Ruler enabled
+134
View File
@@ -0,0 +1,134 @@
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/staebchenfisch)
# Drag Ruler
This module shows a ruler when you drag a token to infrom you how far you've dragged the token from it's start point. Additionally, if you're using a grid, the spaces the token will travel though will be colored depending on your tokens speed. If you're using a gridless map the ruler color will change to convey this information.
## Path color
![Drag Ruler demonstration](https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/5177746fbb4edb28b6ba09137247d142af575c47/media/drag_ruler.webp)
The token has a speed of 30ft. All squares on the path within that range are colored in the players color. If the token is running it can cover double that range (this can be changed in the settings). All Squares on the path that can only be reached while running are colored in yellow. Squares on the path that the token cannot possibly reached at regular speeds are colored red. This coloring behavior can be tweaked in the settings and can be overridden by modules and game systems to provide more granular, game system specific control. For more information on how to do this see the [API](#api) section of this document.
## Waypoints
You can add waypoints to the path by pressing spacebar while you drag the token. To remove a placed waypoint press the right mouse button.
![Demonstration of Waypoints](https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/5177746fbb4edb28b6ba09137247d142af575c47/media/waypoints.webp)
## Why would I want to use this instead of ShowDragDistance?
ShowDragDistance isn't maintained anymore. This means that it is at risk to stop working with every foundry update. In fact this process has already begun. As of Foundry Version 0.7.9 ShowDragDistance doesn't work anymore on gridless maps. Drag Ruler on the other hand is fully compatible with the current Foundry release and I'll continue updating it for future foundry releases for the forseeable future.
In addition Drag Ruler provides more flexibility for game systems and modules via it's api to provide an experience that fits the rules of the game system that you are playing best.
## Game systems with Drag Ruler integration
Drag Ruler will work with all Foundry VTT game systems. However some game systems offer a special integration via the [Drag Ruler API](#api), that allows Drag Ruler to take the rules of the game system into account when dispaying speeds (such as weight carried or conditions that apply to the character), offering a smoother experience. While some game systems offer this integration natively, for other game systems there are modules providing the integration. If the integration is provided via a module you need to install and activate both Drag Ruler and the integration module to benefit from the integration.
The game systems that offer Drag Ruler integration are:
- Cypher System (starting with version 1.13.0)
- Pathfinder 1 (starting with version 0.77.3)
- Pathfinder 2e (via the module [PF2E Drag Ruler Integration](https://foundryvtt.com/packages/pf2e-dragruler/))
## Translations
Drag Ruler is available in the follwing languages:
- English
- Japanese (thanks to touge)
## API
*Audience: This paragraph is intended for module and system devleopers that want to add more complex behavior to Drag Ruler. If you just want to use this plugins features skip this paragraph.*
The path coloring behavior of Drag Ruler can be altered by modules and systems to allow for for more complex coloring than provided by default. This allows specifying custom colors, using more different colors than offered by default and performing more calculations for determining the colors (for example a token may only be allowd to run if it isn't waring armor). Doing so is simple. This paragraph will provide an example by showing an implementation of the api for a fictional game system, that contains everything you need to get started. Afterwards the code will be dissected into small parts and explained.
### Full example
```javascript
Hooks.once("dragRuler.ready", (SpeedProvider) => {
class FictionalGameSystemSpeedProvider extends SpeedProvider {
get colors() {
return [
{id: "walk", default: 0x00FF00, name: "my-module-id.speeds.walk"},
{id: "dash", default: 0xFFFF00, name: "my-module-id.speeds.dash"},
{id: "run", default: 0xFF8000, name: "my-module-id.speeds.run"}
]
}
getRanges(token) {
const baseSpeed = token.actor.data.speed
// A character can always walk it's base speed and dash twice it's base speed
const ranges = [
{range: baseSpeed, color: "walk"},
{range: baseSpeed * 2, color: "dash"}
]
// Characters that aren't wearing armor are allowed to run with three times their speed
if (!token.actor.data.isWearingArmor) {
ranges.push({range: baseSpeed * 3, color: "dash"})
}
return ranges
}
}
dragRuler.registerModule("my-module-id", FictionalGameSystemSpeedProvider)
})
```
### Exmplanation of the code
```javascript
Hooks.once("dragRuler.ready", (SpeedProvider) => {
class FictionalGameSystemSpeedProvider extends SpeedProvider {
```
After Drag Ruler has initialized and is ready to receive API calls it will fire the `dragRuler.ready` event. This is the signal for your module/gamesystem that it can now register itself in Drag Ruler. To do this you'll need to implement a Speed Provider. The Hook will provide you with one Argument: The class `SpeedProvider`, which serves as base class for all speed providers. To implement a Speed Provider you create a subclass of `SpeedProvider`. Within that class you override functions of the base class to implement the functionality you need. The functions `colors` and `getRanges` must be overridden by all Speed Provider implementations. Overriding other functions of the Speed Provider is optional and can be done if you need additional functionality for your speed provider.
```javascript
get colors() {
return [
{id: "walk", default: 0x00FF00, name: "my-module-id.speeds.walk"},
{id: "dash", default: 0xFFFF00, name: "my-module-id.speeds.dash"},
{id: "run", default: 0xFF8000, name: "my-module-id.speeds.run"}
]
}
```
The getter `colors` is one of the two functions that must be overridden by all implementations of `SpeedProvider`. It must return an array of all colors, that may be used by this speed provider. Each color must be an object and has three attributes:
- *id*: A name for this color that identifies this color. It will be used in other functions within your speed provider to reference to this color. Ther must not be two colors with the same id.
- *default*: The default color value that should be used by this color.
- *name*: A human readable name for this color that will be used in the Speed Provider Settings dialog. Drag Ruler will try to internationalize this string. This field is optional, but it's highly recommended to use it.
```javascript
getRanges(token) {
const baseSpeed = token.actor.data.speed
// A character can always walk it's base speed and dash twice it's base speed
const ranges = [
{range: baseSpeed, color: "walk"},
{range: baseSpeed * 2, color: "dash"}
]
// Characters that aren't wearing armor are allowed to run with three times their speed
if (!token.actor.data.isWearingArmor) {
ranges.push({range: baseSpeed * 3, color: "dash"})
}
return ranges
}
```
The `getRanges` function is the second function that every Speed Provider must override. This function receives a token as a parameter and must return an array of all the ranges that this token can reach. Each range is represented by an object having these fields:
- *range*: The maximum distance a token is allowed to move within this range
- *color*: The id of the color that is used to represent this range. This id must match the id as defined in the `colors` getter.
```javascript
dragRuler.registerModule("my-module-id", FictionalGameSystemSpeedProvider)
```
This line registers the Speed Provider class that was just created with Drag Ruler. The paramter must be the id of the module you're writing. This id must exactly match the id specified in you manifest. As the second parameter the Speed Provider class that was just created is passed in.
If you're not writing a module but a game system use `dragRuler.registerSystem` instead of `dragRuler.registerModule.
### Additional capabilities of the API
In addition to the basic capabilities of the API presented in the example above, Drag Ruler's API offers more capabilities, like adding settings to your Speed Provider. To learn more about additional capabilities refer to the documentation of the `SpeedProvider` base class in [speed_provider.js](src/speed_provider.js).
+55 -16
View File
@@ -1,22 +1,61 @@
{ {
"drag-ruler": { "drag-ruler": {
"settings": { "genericSpeedProvider": {
"dashMultiplier": { "settings": {
"name": "Dash Multiplier", "dashMultiplier": {
"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." "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", "speedAttribute": {
"hint": "The attribute that defines a token's walking speed. This is used during coloring of the measured path." "name": "Speed Attribute",
}, "hint": "The attribute that defines a token's walking speed. This is used during coloring of the measured path."
"speedProvider": {
"name": "Speed Settings Provider",
"hint": "Select who provides speed information for tokens duing coloring. Using a game system or module may provide more flexible coloring than sticking to the options provided by Drag Ruler.",
"choices": {
"module": "Module",
"native": "Drag Ruler",
"system": "System"
} }
},
"speeds": {
"walk": "walk",
"dash": "dash"
}
},
"settings": {
"alwaysShowSpeedForPCs": {
"name": "Show PC speed to everyone",
"hint": "If enabled the coloring based on actor speed for player characters will shown to everyone, even if they don't have observer permission for the character sheet."
},
"speedProviderSettings": {
"name": "Speed Provider Settings",
"hint": "The Speed Provider Settings contain all the game system specific settings.",
"button": "Speed Provider Settings",
"windowTitle": "Speed Provider Settings",
"headers": {
"speedProvider": "Speed Provider",
"speedProviderSettings": "Speed Provider specific settings"
},
"activeProvider": {
"name": "Currently active Speed Provider",
"hint": "The settings show below depend on the active speed provider. If the GM selects a different speed provider the available settings may change."
},
"noSettings": "This speed provider doesn't offer any configuration options.",
"color": {
"name": "Color for {colorName}",
"hint": "The color that will be used to color square 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}"
}
}
},
"swapSpacebarRightClick": {
"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"
} }
} }
} }
+35
View File
@@ -0,0 +1,35 @@
{
"drag-ruler": {
"settings": {
"alwaysShowSpeedForPCs": {
"name": "プレイヤーキャラクターの移動速度を全員に公開する",
"hint": "有効にすると、アクターの観察者(Observer)権限を持たない人にも,移動範囲の色分けが表示されるようになります。"
},
"dashMultiplier": {
"name": "早足の倍率",
"hint": "有効にすると、移動範囲に二つ目の色分けが追加されます。0にすると、この機能は無効になります。"
},
"speedAttribute": {
"name": "移動速度の属性値",
"hint": "コマの移動速度が定義された属性値を指定します。移動範囲表示の色分けに使用されます。"
},
"speedProvider": {
"name": "移動速度設定の提供元",
"hint": "色分けに使用する速度情報の提供元を選択します。ゲームシステムやモジュールを選択すると、Drag Ruler自体が提供するものより柔軟な色分けが行える可能性があります。",
"choices": {
"module": "モジュール",
"native": "Drag Ruler",
"system": "ゲームシステム"
}
},
"staticFirstColor": {
"name": "一つ目の色を固定する",
"hint": "一つ目の移動範囲の色に、プレイヤーの色でなく固定された色を使用します。"
},
"swapSpacebarRightClick": {
"name": "スペースキーと右クリックを入れ替える",
"hint": "ドラッグ中のスペースキーと右クリックの機能を入れ替えます。有効にすると右クリックがウェイポイントの配置、スペースキーが削除になります。"
}
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 689 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 775 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 603 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 503 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

+11 -2
View File
@@ -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": "0.0.1", "version": "1.3.0",
"minimumCoreVersion" : "0.7.9", "minimumCoreVersion" : "0.7.9",
"compatibleCoreVersion" : "0.7.9", "compatibleCoreVersion" : "0.7.9",
"authors": [ "authors": [
@@ -15,16 +15,25 @@
"esmodules": [ "esmodules": [
"src/main.js" "src/main.js"
], ],
"templates": [
"speed_provider_settings.html"
],
"languages": [ "languages": [
{ {
"lang": "en", "lang": "en",
"name": "English", "name": "English",
"path": "lang/en.json" "path": "lang/en.json"
},
{
"lang": "ja",
"name": "日本語",
"path": "lang/ja.json"
} }
], ],
"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/master.zip", "download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/v1.3.0.zip",
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/master/module.json", "manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/master/module.json",
"readme": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/README.md", "readme": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/README.md",
"changelog": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/CHANGELOG.md",
"bugs": "https://github.com/manuelVo/foundryvtt-drag-ruler/issues" "bugs": "https://github.com/manuelVo/foundryvtt-drag-ruler/issues"
} }
+69 -16
View File
@@ -1,37 +1,61 @@
import {GenericSpeedProvider, SpeedProvider} from "./speed_provider.js"
import {settingsKey} from "./settings.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 +64,37 @@ 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 registerModule(moduleId, speedProvider) { export function registerModule(moduleId, speedProvider) {
+19
View File
@@ -0,0 +1,19 @@
export function getHexSizeSupportTokenGridCenter(token) {
const tokenCenterOffset = CONFIG.hexSizeSupport.getCenterOffset(token)
const tokenCenter = {x: token.x + tokenCenterOffset.x, y: token.y + tokenCenterOffset.y}
if (token.getFlag("hex-size-support", "borderSize") % 2 === 1)
return tokenCenter
if (canvas.grid.grid.columns) {
let hexOffset = canvas.grid.w / 2
if (!CONFIG.hexSizeSupport.getAltOrientationFlag(token))
hexOffset *= -1
tokenCenter.x += hexOffset
}
else {
let hexOffset = canvas.grid.h / 2
if (CONFIG.hexSizeSupport.getAltOrientationFlag(token))
hexOffset *= -1
tokenCenter.y += hexOffset
}
return tokenCenter
}
+179
View File
@@ -0,0 +1,179 @@
import {getColorForDistance} from "./main.js"
// This is a modified version of Ruler.moveToken from foundry 0.7.9
export async function moveTokens(draggedToken, selectedTokens) {
let wasPaused = game.paused;
if (wasPaused && !game.user.isGM) {
ui.notifications.warn(game.i18n.localize("GAME.PausedWarning"));
return false;
}
if (!this.visible || !this.destination) return false;
if (!draggedToken) return;
// Get the movement rays and check collision along each Ray
// These rays are center-to-center for the purposes of collision checking
const rays = this._getRaysFromWaypoints(this.waypoints, this.destination);
if (!game.user.isGM) {
const hasCollision = selectedTokens.some(token => {
const offset = calculateTokenOffset(token, draggedToken)
const offsetRays = rays.map(ray => applyOffsetToRay(ray, offset))
return offsetRays.some(r => canvas.walls.checkCollision(r));
})
if (hasCollision) {
ui.notifications.error(game.i18n.localize("ERROR.TokenCollide"));
this._endMeasurement();
return true;
}
}
// Execute the movement path.
// Transform each center-to-center ray into a top-left to top-left ray using the prior token offsets.
this._state = Ruler.STATES.MOVING;
await Promise.all(selectedTokens.map(token => {
// Return the promise so we can wait for it outside the loop
const offset = calculateTokenOffset(token, draggedToken)
return animateToken.call(this, token, rays, offset, wasPaused)
}))
// Once all animations are complete we can clear the ruler
this._endMeasurement();
}
// This is a modified version code extracted from Ruler.moveToken from foundry 0.7.9
async function animateToken(token, rays, tokenOffset, wasPaused) {
const offsetRays = rays.map(ray => applyOffsetToRay(ray, tokenOffset))
// Determine offset relative to the Token top-left.
// This is important so we can position the token relative to the ruler origin for non-1x1 tokens.
const origin = [this.waypoints[0].x + tokenOffset.x, this.waypoints[0].y + tokenOffset.y]
let dx, dy
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
dx = token.data.x - origin[0]
dy = token.data.y - origin[1]
}
else {
dx = token.data.x - origin[0]
dy = token.data.y - origin[1]
}
token._noAnimate = true;
for (let r of offsetRays) {
if (!wasPaused && game.paused) break;
const dest = [r.B.x, r.B.y];
const path = new Ray({ x: token.x, y: token.y }, { x: dest[0] + dx, y: dest[1] + dy });
await token.update(path.B);
await token.animateMovement(path);
}
token._noAnimate = false;
}
function calculateTokenOffset(tokenA, tokenB) {
return {x: tokenA.data.x - tokenB.data.x, y: tokenA.data.y - tokenB.data.y}
}
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})
}
// This is a modified version of Ruler._onMouseMove from foundry 0.7.9
export function onMouseMove(event) {
if (this._state === Ruler.STATES.MOVING) return;
// Extract event data
const mt = event._measureTime || 0;
const originalEvent = event.data.originalEvent;
const destination = {x: event.data.destination.x + this.rulerOffset.x, y: event.data.destination.y + this.rulerOffset.y}
// Hide any existing Token HUD
canvas.hud.token.clear();
delete event.data.hudState;
// Draw measurement updates
if (Date.now() - mt > 50) {
this.measure(destination, {snap: !originalEvent.shiftKey});
event._measureTime = Date.now();
this._state = Ruler.STATES.MEASURING;
}
}
// This is a modified version of Ruler.measure form foundry 0.7.9
export function measure(destination, {gridSpaces=true, snap=false} = {}) {
if (this.isDragRuler && !this.draggedToken.isVisible)
return []
if (snap)
destination = new PIXI.Point(...canvas.grid.getCenter(destination.x, destination.y));
const waypoints = this.waypoints.concat([destination]);
const centeredWaypoints = waypoints.map(w => new PIXI.Point(...canvas.grid.getCenter(w.x, w.y)))
const r = this.ruler;
this.destination = destination;
// Iterate over waypoints and construct segment rays
const segments = [];
const centeredSegments = []
for (let [i, dest] of waypoints.slice(1).entries()) {
const centeredDest = centeredWaypoints[i + 1]
const origin = waypoints[i];
const centeredOrigin = centeredWaypoints[i]
const label = this.labels.children[i];
const ray = new Ray(origin, dest);
const centeredRay = new Ray(centeredOrigin, centeredDest)
if (ray.distance < 10) {
if (label) label.visible = false;
continue;
}
segments.push({ ray, label });
centeredSegments.push({ray: centeredRay, label})
}
// Compute measured distance
const distances = canvas.grid.measureDistances(centeredSegments, { gridSpaces });
let totalDistance = 0;
for (let [i, d] of distances.entries()) {
let s = segments[i];
s.startDistance = totalDistance
totalDistance += d;
s.last = i === (segments.length - 1);
s.distance = d;
s.text = this._getSegmentLabel(d, totalDistance, s.last);
}
// Clear the grid highlight layer
const hlt = canvas.grid.highlightLayers[this.name] || canvas.grid.addHighlightLayer(this.name);
hlt.clear();
// Draw measured path
r.clear();
let rulerColor
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS)
rulerColor = getColorForDistance.call(this, totalDistance)
else
rulerColor = this.color
for (let s of segments) {
const { ray, label, text, last } = s;
// Draw line segment
r.lineStyle(6, 0x000000, 0.5).moveTo(ray.A.x, ray.A.y).lineTo(ray.B.x, ray.B.y)
.lineStyle(4, rulerColor, 0.25).moveTo(ray.A.x, ray.A.y).lineTo(ray.B.x, ray.B.y);
// Draw the distance label just after the endpoint of the segment
if (label) {
label.text = text;
label.alpha = last ? 1.0 : 0.5;
label.visible = true;
let labelPosition = ray.project((ray.distance + 50) / ray.distance);
label.position.set(labelPosition.x, labelPosition.y);
}
// Highlight grid positions
this._highlightMeasurement(ray, s.startDistance);
}
// Draw endpoints
for (let p of waypoints) {
r.lineStyle(2, 0x000000, 0.5).beginFill(rulerColor, 0.25).drawCircle(p.x, p.y, 8);
}
// Return the measured segments
return segments;
}
+166 -60
View File
@@ -1,36 +1,40 @@
"use strict" "use strict"
import {availableSpeedProviders, currentSpeedProvider, registerModule, registerSystem, setCurrentSpeedProvider} from "./api.js" import {currentSpeedProvider, getRangesFromSpeedProvider, getUnreachableColorFromSpeedProvider, initApi, registerModule, registerSystem} from "./api.js"
import {getHexSizeSupportTokenGridCenter} from "./compatibility.js"
import {measure, moveTokens, onMouseMove} from "./foundry_imports.js"
import {performMigrations} from "./migration.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()
hookRulerHandlers() hookRulerFunctions()
patchRulerMeasure() hookKeyboardManagerFunctions()
patchRulerHighlightMeasurement() patchRulerHighlightMeasurement()
availableSpeedProviders["native"] = nativeSpeedProvider
setCurrentSpeedProvider(nativeSpeedProvider)
window.dragRuler = { window.dragRuler = {
getColorForDistance, getColorForDistance,
registerModule, registerModule,
registerSystem registerSystem,
} }
}) })
Hooks.once("ready", () => { Hooks.once("ready", () => {
Hooks.callAll("dragRuler.ready") performMigrations()
Hooks.callAll("dragRuler.ready", SpeedProvider)
}) })
Hooks.on("canvasReady", () => { Hooks.on("canvasReady", () => {
canvas.controls.ruler.draggedToken = null canvas.controls.rulers.children.forEach(ruler => {
Object.defineProperty(canvas.controls.ruler, "isDragRuler", { ruler.draggedToken = null
get: function isDragRuler() { Object.defineProperty(ruler, "isDragRuler", {
return Boolean(this.draggedToken) // If draggedToken is set this is a drag ruler get: function isDragRuler() {
} return Boolean(this.draggedToken) // If draggedToken is set this is a drag ruler
}
})
}) })
}) })
@@ -62,7 +66,7 @@ function hookTokenDragHandlers() {
} }
} }
function hookRulerHandlers() { function hookRulerFunctions() {
const originalMoveTokenHandler = Ruler.prototype.moveToken const originalMoveTokenHandler = Ruler.prototype.moveToken
Ruler.prototype.moveToken = function (event) { Ruler.prototype.moveToken = function (event) {
const eventHandled = onRulerMoveToken.call(this, event) const eventHandled = onRulerMoveToken.call(this, event)
@@ -70,99 +74,201 @@ function hookRulerHandlers() {
return originalMoveTokenHandler.call(this, event) return originalMoveTokenHandler.call(this, event)
return true return true
} }
const originalToJSON = Ruler.prototype.toJSON
Ruler.prototype.toJSON = function () {
const json = originalToJSON.call(this)
if (this.draggedToken)
json["draggedToken"] = this.draggedToken.data._id
return json
}
const originalUpdate = Ruler.prototype.update
Ruler.prototype.update = function (data) {
if (data.draggedToken) {
this.draggedToken = canvas.tokens.get(data.draggedToken)
}
originalUpdate.call(this, data)
}
const originalMeasure = Ruler.prototype.measure
Ruler.prototype.measure = function (destination, options={}) {
if (this.isDragRuler) {
return measure.call(this, destination, options)
}
else {
return originalMeasure.call(this, destination, options)
}
}
}
function hookKeyboardManagerFunctions() {
const originalHandleKeys = KeyboardManager.prototype._handleKeys
KeyboardManager.prototype._handleKeys = function (event, key, up) {
const eventHandled = handleKeys.call(this, event, key, up)
if (!eventHandled)
originalHandleKeys.call(this, event, key, up)
}
}
function handleKeys(event, key, up) {
if (event.repeat || this.hasFocus)
return false
if (key.toLowerCase() === "x") return onKeyX(up)
if (key.toLowerCase() === "shift") return onKeyShift(up)
return false
}
function onKeyX(up) {
if (up)
return false
if (!canvas.controls.ruler.isDragRuler)
return false
deleteWaypoint()
return true
}
function onKeyShift(up) {
const ruler = canvas.controls.ruler
if (!ruler.isDragRuler)
return false
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens)
const rulerOffset = ruler.rulerOffset
const measurePosition = {x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y}
ruler.measure(measurePosition, {snap: up})
} }
function onTokenLeftDragStart(event) { function onTokenLeftDragStart(event) {
canvas.controls.ruler._onDragStart(event) if (!currentSpeedProvider.usesRuler(this))
canvas.controls.ruler.draggedToken = this return
const ruler = canvas.controls.ruler
ruler.draggedToken = this
let tokenCenter
if (canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(this))
tokenCenter = getHexSizeSupportTokenGridCenter(this)
else
tokenCenter = {x: this.x + canvas.grid.grid.w / 2, y: this.y + canvas.grid.grid.h / 2}
ruler.clear();
ruler._state = Ruler.STATES.STARTING;
ruler.rulerOffset = {x: tokenCenter.x - event.data.origin.x, y: tokenCenter.y - event.data.origin.y}
addWaypoint.call(ruler, tokenCenter, false)
} }
function onTokenLeftDragMove(event) { function onTokenLeftDragMove(event) {
if (canvas.controls.ruler.isDragRuler) const ruler = canvas.controls.ruler
canvas.controls.ruler._onMouseMove(event) if (ruler.isDragRuler)
onMouseMove.call(ruler, event)
} }
function onTokenDragLeftDrop(event) { function onTokenDragLeftDrop(event) {
if (!canvas.controls.ruler.isDragRuler) const ruler = canvas.controls.ruler
if (!ruler.isDragRuler)
return false return false
canvas.controls.ruler.draggedToken = null const selectedTokens = canvas.tokens.controlled
canvas.controls.ruler.moveToken(event) moveTokens.call(ruler, ruler.draggedToken, selectedTokens)
ruler.draggedToken = null
return true return true
} }
function onTokenDragLeftCancel(event) { function onTokenDragLeftCancel(event) {
if (!canvas.controls.ruler.isDragRuler) // This function is invoked by right clicking
const ruler = canvas.controls.ruler
if (!ruler.isDragRuler)
return false return false
if (canvas.controls.ruler._state === Ruler.STATES.MEASURING) { if (ruler._state === Ruler.STATES.MEASURING) {
if (canvas.controls.ruler.waypoints.length > 1) { if (!game.settings.get(settingsKey, "swapSpacebarRightClick")) {
canvas.controls.ruler._removeWaypoint(canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens), {snap: !event.shiftKey}) if (ruler.waypoints.length > 1)
game.user.broadcastActivity({ruler: canvas.controls.ruler}) event.preventDefault()
event.preventDefault() deleteWaypoint()
} }
else { else {
canvas.controls.ruler._endMeasurement() event.preventDefault()
canvas.controls.ruler.draggedToken = null const snap = !event.shiftKey
return false addWaypoint.call(ruler, ruler.destination, snap)
} }
} }
return true return true
} }
function onRulerMoveToken(event) { function onRulerMoveToken(event) {
// This function is invoked by left clicking
if (!this.isDragRuler) if (!this.isDragRuler)
return false return false
this._addWaypoint(this.destination) if (!game.settings.get(settingsKey, "swapSpacebarRightClick")) {
const snap = !event.shiftKey
addWaypoint.call(this, this.destination, snap)
}
else
deleteWaypoint()
return true 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) { function strInsertAfter(haystack, needle, strToInsert) {
const pos = haystack.indexOf(needle) + needle.length const pos = haystack.indexOf(needle) + needle.length
return haystack.slice(0, pos) + strToInsert + haystack.slice(pos) return haystack.slice(0, pos) + strToInsert + haystack.slice(pos)
} }
// These patches were written with foundry-0.7.9.js as reference export function getColorForDistance(startDistance, subDistance=0) {
function patchRulerMeasure() {
let code = Ruler.prototype.measure.toString()
// Remove function signature and closing curly bracket (those are on the first and last line)
code = code.slice(code.indexOf("\n"), code.lastIndexOf("\n"))
code = strInsertAfter(code, "for ( let [i, d] of distances.entries() ) {\n", "segments[i].startDistance = totalDistance\n")
code = strInsertAfter(code, "this._highlightMeasurement(ray", ", s.startDistance")
Ruler.prototype.measure = new Function("destination", "{gridSpaces=true}={}", code)
}
function nativeSpeedProvider(token, playercolor) {
const speedAttribute = game.settings.get(settingsKey, "speedAttribute")
if (!speedAttribute)
return []
const tokenSpeed = getProperty(token, speedAttribute)
if (tokenSpeed === undefined) {
console.warn(`Drag Ruler | The configured token speed attribute "${speedAttribute}" didn't return a speed value. To use colors based on drag distance set the setting to the correct value (or clear the box to disable this feature).`)
return []
}
const dashMultiplier = game.settings.get(settingsKey, "dashMultiplier")
if (!dashMultiplier)
return [{range: tokenSpeed, color: playercolor}]
return [{range: tokenSpeed, color: playercolor}, {range: tokenSpeed * dashMultiplier, color: 0xFFFF00}]
}
function getColorForDistance(startDistance, subDistance) {
if (!this.isDragRuler) if (!this.isDragRuler)
return this.color return this.color
// Don't apply colors if the current user doesn't have at least observer permissions
if (this.draggedToken.actor.permission < 2) {
// If this is a pc and alwaysShowSpeedForPCs is enabled we show the color anyway
if (!(this.draggedToken.actor.data.type === "character" && game.settings.get(settingsKey, "alwaysShowSpeedForPCs")))
return this.color
}
const distance = startDistance + subDistance const distance = startDistance + subDistance
const ranges = currentSpeedProvider(this.draggedToken, this.color) const ranges = getRangesFromSpeedProvider(this.draggedToken)
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 // These patches were written with foundry-0.7.9.js as reference
function patchRulerHighlightMeasurement() { function patchRulerHighlightMeasurement() {
let code = Ruler.prototype._highlightMeasurement.toString() 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) // Remove function signature and closing curly bracket (those are on the first and last line)
code = code.slice(code.indexOf("\n"), code.lastIndexOf("\n")) code = code.slice(code.indexOf("\n"), code.lastIndexOf("\n"))
+27
View File
@@ -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
}
}
+208 -27
View File
@@ -1,38 +1,219 @@
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() {
// This setting will be modified by the api if modules register to it game.settings.register(settingsKey, "dataVersion", {
game.settings.register(settingsKey, "speedProvider", {
name: "drag-ruler.settings.speedProvider.name",
hint: "drag-ruler.settings.speedProvider.hint",
scope: "world", scope: "world",
config: false, config: false,
type: Object, type: String,
choices: { default: "fresh install"
"native": game.i18n.localize("drag-ruler.settings.speedProvider.choices.native") })
},
default: "native", game.settings.register(settingsKey, "swapSpacebarRightClick", {
name: "drag-ruler.settings.swapSpacebarRightClick.name",
hint: "drag-ruler.settings.swapSpacebarRightClick.hint",
scope: "client",
config: true,
type: Boolean,
default: false,
})
game.settings.register(settingsKey, "alwaysShowSpeedForPCs", {
name: "drag-ruler.settings.alwaysShowSpeedForPCs.name",
hint: "drag-ruler.settings.alwaysShowSpeedForPCs.hint",
scope: "world",
config: true,
type: Boolean,
default: true,
})
// 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, onChange: updateSpeedProvider,
}) })
game.settings.register(settingsKey, "speedAttribute", { game.settings.registerMenu(settingsKey, "speedProviderSettings", {
name: "drag-ruler.settings.speedAttribute.name", name: "drag-ruler.settings.speedProviderSettings.name",
hint: "drag-ruler.settings.speedAttribute.hint", hint: "drag-ruler.settings.speedProviderSettings.hint",
scope: "world", label: "drag-ruler.settings.speedProviderSettings.button",
config: true, icon: "fas fa-tachometer-alt",
type: String, type: SpeedProviderSettings,
default: getDefaultSpeedAttribute(), restricted: false,
})
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(),
}) })
} }
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) {
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(formData.speedProvider))
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)
}
+144
View File
@@ -0,0 +1,144 @@
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 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
}
/**
* 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 = 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: playercolor}]
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(),
}
]
}
}
+3
View File
@@ -3,6 +3,8 @@ export function getDefaultSpeedAttribute() {
switch (game.system.id) { switch (game.system.id) {
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"
} }
@@ -12,6 +14,7 @@ export function getDefaultSpeedAttribute() {
export function getDefaultDashMultiplier() { export function getDefaultDashMultiplier() {
switch (game.system.id) { switch (game.system.id) {
case "dnd5e": case "dnd5e":
case "lancer":
case "pf1": case "pf1":
return 2 return 2
} }
+63
View File
@@ -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>