Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6027735f4c | |||
| 84bce39043 | |||
| 46c504341c | |||
| ff9b187d90 | |||
| bd76c965a2 | |||
| ee9ac14346 | |||
| 7a00d6fcc2 | |||
| 4957188630 | |||
| 944ff96a26 | |||
| 58565b7011 | |||
| 25fc6258e7 | |||
| ac00aac85b | |||
| 8d5a6716e1 | |||
| c43b0bfd64 | |||
| c21e4c91d6 | |||
| 4b30d52da1 | |||
| 08c39b5683 | |||
| 2e2353e1d1 | |||
| cf74a11f18 | |||
| a1c10e088d | |||
| 626829e989 | |||
| b87e91b63c | |||
| ed10144cc6 | |||
| 5e82049686 | |||
| 71424cd284 | |||
| 445c03d29a | |||
| 5904efbdd5 | |||
| d325f8acee | |||
| 1577435a33 | |||
| 1b8b2328c6 | |||
| 0dba39b145 | |||
| c1b0e9d7fb | |||
| 9d227efcd1 | |||
| a3d942c287 | |||
| bdbd4fde18 | |||
| 3afb1f30b7 | |||
| 654f8e3111 | |||
| 2979e86201 | |||
| 8f81fd707e | |||
| 5f6e348bf0 | |||
| 82f2a885fb | |||
| d536ae8dfb | |||
| 5add0be393 | |||
| f77990420a | |||
| 44efabc656 | |||
| e84632c0d7 | |||
| 30bd3d33e0 | |||
| 6604e322bd | |||
| b68f3bbe29 | |||
| 793e87bc2f | |||
| df4c515fcd | |||
| 02dfd8db0d | |||
| 65911c11f0 | |||
| ddf89f9499 | |||
| 0dfdb23bfb | |||
| 3f2e9e1a3e | |||
| 846e39d946 | |||
| 26748ee234 | |||
| ebd0ee6ccf | |||
| 1d477094b8 | |||
| bbfce2bb77 | |||
| 54cee52eb2 | |||
| a967a03197 | |||
| 8d243ce919 | |||
| b227d5073b | |||
| 576db2dc5a | |||
| b031acaa6e | |||
| ee956abd78 | |||
| 1334703877 | |||
| d6cccf4466 | |||
| 074d2f5052 | |||
| e5b8f896f8 | |||
| 3de350d3e9 | |||
| 8f97081236 | |||
| 18253dc42b | |||
| fb702cd850 | |||
| ad3fdf4d18 | |||
| 58946096a6 | |||
| 22112ddfd5 | |||
| f59b0f9d2d | |||
| 443cfd4317 | |||
| fb3665d758 | |||
| 96cb690431 | |||
| fb7202d5f0 | |||
| fa581ddfc3 | |||
| 56e15be4ca | |||
| 1509307313 | |||
| 68885385ea | |||
| 6748617f6a | |||
| 9d5dccb504 | |||
| a252da620a | |||
| 41c8979925 | |||
| 39f9204fa7 | |||
| 10633e4e2a | |||
| f3e1492edd | |||
| 7ac1c828b6 | |||
| 1972498b05 | |||
| 4a5b9bbb11 | |||
| d467fe5bcf | |||
| 1f770f7597 | |||
| fa62e1d9c4 |
@@ -0,0 +1,5 @@
|
||||
/foundry*.js
|
||||
artifact/
|
||||
wasm/
|
||||
*.lock
|
||||
.vscode/
|
||||
@@ -0,0 +1,4 @@
|
||||
printWidth: 100
|
||||
trailingComma: "all"
|
||||
bracketSpacing: false
|
||||
arrowParens: "avoid"
|
||||
+123
@@ -1,3 +1,126 @@
|
||||
## 1.13.0
|
||||
### Breaking changes
|
||||
- Drag Ruler's pathfinder has been extracted into a dedicated library module. If you'd like to continue to use Drag Ruler's pathfinding feature, please install [routinglib](https://foundryvtt.com/packages/routinglib) alongside Drag Ruler.
|
||||
- Drag Ruler's API no longer supports the function `getCostForStep`. Instead, module authors are asked to use [Enhanced Terrain Layer's game system integration API](https://github.com/ironmonk88/enhanced-terrain-layer/blob/main/README.md#integrating-game-system-rules) to introduce game system specific terrain rules.
|
||||
|
||||
### New features
|
||||
- Drag Ruler's pathfinding will now be running as a background task. This means that Foundry will no longer freeze while Drag Ruler is calculating a path.
|
||||
- Drag Ruler's pathfinding will now take difficult terrain into account on griddes scenes
|
||||
|
||||
### Bug fixes
|
||||
- Fixed a bug that would cause gridless snapping to snap slightly below the allowed range
|
||||
|
||||
### Compatibility
|
||||
- Drag Ruler is now compatible with Foundry VTT v10
|
||||
- Drag Ruler's compatibility with the Wall Height module is restored
|
||||
|
||||
|
||||
## 1.12.8
|
||||
### Bugfixes
|
||||
- Fixed a bug that could cause grid cells to be highlighted in the wrong color
|
||||
|
||||
### Compatibility
|
||||
- Fixed an interaction with the Wall Height module that could allow players to walk through walls
|
||||
|
||||
|
||||
## 1.12.7
|
||||
### Bugfixes
|
||||
- Fixed a bug that caused measurement templates to only snap to the corners of the grid (this was a regression introduced in 1.12.5)
|
||||
|
||||
|
||||
## 1.12.6
|
||||
### Bugfixes
|
||||
- Measured templates no longer snap to a virtual grid on gridless scenes (this was a regression introduced in 1.12.5)
|
||||
|
||||
|
||||
## 1.12.5
|
||||
### Compatibility
|
||||
- Drag Ruler is now compatible with DF Template Enhancements
|
||||
|
||||
|
||||
## 1.12.4
|
||||
### Bugfixes
|
||||
- Fixed a bug that could cause Drag Ruler to override the default ruler color on other player's clients
|
||||
|
||||
|
||||
## 1.12.3
|
||||
### Bugfixes
|
||||
- Fixed a bug that could cause foundry to freeze indefinitely when trying to pathfind to an unreachalbe location (thanks to JDCalvert)
|
||||
- Fixed a bug that caused the pathfinder to route through one-directional walls from the wrong direction (thanks to JDCalvert)
|
||||
- Fixed a bug that could cause Drag Ruler to write errors into the JS console during regular usage
|
||||
|
||||
### Compatibility
|
||||
- Drag Ruler's generic speed provider is now aware of good defaults for DnD 4th Edition
|
||||
- Drag Ruler's pathfinder should now be compatible with the Wall Height and Levels modules (thanks to JDCalvert)
|
||||
|
||||
|
||||
## 1.12.2
|
||||
### Bugfixes
|
||||
- Fixed a bug where the pathfinder on gridless scenes sometimes wasn't able to find a way around corners with specific angles
|
||||
- Pathfinding will now be disabled when the hotkey to move tokens without animation is being pressed, to allow GMs to move their tokens through walls
|
||||
|
||||
|
||||
## 1.12.1
|
||||
### Hotfix
|
||||
- Version 1.12.0 was incorrectly packaged, which caused it to fail to load
|
||||
|
||||
|
||||
## 1.12.0
|
||||
### New features
|
||||
- Pathfinding is now supported on gridless scenes
|
||||
|
||||
|
||||
## 1.11.5
|
||||
### Bugfixes
|
||||
- Fixed a bug that was causing Drag Ruler to spam useless warnings into the console (this was a regression introduced in 1.11.4)
|
||||
|
||||
|
||||
## 1.11.4
|
||||
### Bugfixes
|
||||
- When changing the measurement mode via a keybinding (toggle snaping or toggle pathfinding) the updated ruler will now be sent to other players immediately
|
||||
- Fixed a bug that incorrectly showed a ruler to be snapped to other players despite the ruler not being snapped
|
||||
- Fixed a bug that could cause a token to move to an incorrect location if the token was being dragged and dropped very rapidly
|
||||
- Drag Ruler's token movement animations can now be properly waited for (this improves the interaction with modules like sequencer)
|
||||
|
||||
### Translation
|
||||
- Updated Spanish translation (thanks to Viriato139ac#342)
|
||||
|
||||
|
||||
## 1.11.3
|
||||
### Bugfixes
|
||||
- The setting to automatically start pathfinding is now visible to players again (this was a regression introduced in 1.11.2)
|
||||
- Fixed a bug that would show the measurements of other players as if they were using the pathfinder, even if they were not using it.
|
||||
|
||||
|
||||
## 1.11.2
|
||||
### Bugfixes
|
||||
- Fixed a memory leak that could cause the rule to slow down after using the pathfinding functionality for a while
|
||||
|
||||
### Misc
|
||||
- GMs are now always allowed to use the pathfinding tool. The setting now only prevents players from using it.
|
||||
|
||||
### Compatibility
|
||||
- Drag Ruler's generic speed provider is now aware of good defaults for Dungeonslayers 4
|
||||
|
||||
|
||||
## 1.11.1
|
||||
### Bugfixes
|
||||
- Fixed a bug that would cause the pathfinding algorithm to make tokens of size 2 and 4 to take an unnecessary step
|
||||
|
||||
|
||||
## 1.11.0
|
||||
### New features
|
||||
- Drag Ruler now supports pathfinding. Pressing the assigned button will automatically calculate the shortest route to the point you're dragging your token to and add the necessary waypoints to the ruler.
|
||||
- This feature is only available for gridded maps
|
||||
- This feature can only be used if it's enabled by the GM in the module settings
|
||||
- The routing algorithm *does not* take difficult terrain into account
|
||||
|
||||
|
||||
## 1.10.3
|
||||
### Compatibility
|
||||
- This release contains changes required to be compatible with Foundry 9.245
|
||||
|
||||
|
||||
## 1.10.2
|
||||
### Bugfixes
|
||||
- Fixed a bug that could cause squares to be highlighted in the wrong color when using waypoints on a 5/10/5 gird
|
||||
|
||||
@@ -32,24 +32,38 @@ During combat, Drag Ruler will remember the path a token has taken during it's t
|
||||

|
||||
|
||||
|
||||
## Pathfinding
|
||||
**To use pathfinding you must install the [routinglib](https://foundryvtt.com/packages/routinglib) module**
|
||||
|
||||
When routinglib is installed, Drag Ruler can automatically place waypoints to walk around walls and terrain to reach the destination with the shortest possible movement. Pathfinding can be activated using a configurable key. Alternatively, Drag Ruler can be configured to always use pathfinding when a token is being dragged.
|
||||
|
||||
Pathfinding is restricted to GM users by default, since the pathfinding algorithm can create ways that lead through unexplored fog of war. If you want to allow your players to use Drag Ruler's pathfinding functionality, you need to enable the associated setting in Drag Ruler's module settings.
|
||||
|
||||

|
||||
|
||||
|
||||
## 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)
|
||||
- DnD5e (via the module [DnD5e Drag Ruler Integration](https://foundryvtt.com/packages/elevation-drag-ruler))
|
||||
- GURPS 4th Edition Game Aid (Unofficial) (starting with version 0.9.1)
|
||||
- Hackmaster (starting with version 0.2.11)
|
||||
- Ironclaw Second Edition (starting with version 0.2.2)
|
||||
- Lancer (via the module [Lancer Speed Provider](https://foundryvtt.com/packages/lancer-speed-provider))
|
||||
- Level Up: Advanced 5th Edition (Official) (via the module [A5E Drag Ruler Integration](https://foundryvtt.com/packages/a5edragruler))
|
||||
- Pathfinder 1 (starting with version 0.77.3)
|
||||
- Pathfinder 2e (via the module [PF2E Drag Ruler Integration](https://foundryvtt.com/packages/pf2e-dragruler/))
|
||||
- Shadowrun 5th Edition (via the module [Drag Ruler Integration for Shadowrun 5E](https://foundryvtt.com/packages/drag-ruler-integration-for-shadowrun-5e))
|
||||
- Shadow of the Demon Lord (starting with version 1.7.15)
|
||||
- Starfinder (via the module [Starfinder Drag Ruler Integration](https://foundryvtt.com/packages/starfinder-drag-ruler))
|
||||
- Stargate RPG (starting with version 1.6.0)
|
||||
- Symbaroum (via the module [Symbaroum drag ruler integration](https://foundryvtt.com/packages/symbaroum-drag-ruler-integration))
|
||||
- Tagmar RPG (starting with version 1.1.4)
|
||||
- TheWitcherTRPG (starting with version 0.0.62)
|
||||
- Tormenta20 (starting with version 1.1.37)
|
||||
- The Dark Eye 5 / Das Schwarze Auge 5 (via the module [TDE5/DSA5 Drag Ruler Integration](https://foundryvtt.com/packages/dsa5-drag-ruler))
|
||||
- Shadow of the Demon Lord (starting with version 1.7.15)
|
||||
- Wasteland Ventures (starting with version 0.1.0)
|
||||
- WWII:OWB (starting with version 1.0.4)
|
||||
|
||||
@@ -40,9 +40,17 @@
|
||||
"moveWithoutAnimation": {
|
||||
"name": "Token animation deaktivieren",
|
||||
"hint": "Wenn diese Taste gedrückt wird, während ein Token fallen gelassen wird, bewegt es sich ohne Animation zum Zielort."
|
||||
},
|
||||
"togglePathfinding": {
|
||||
"name": "Wegfindung umschalten",
|
||||
"hint": "Wenn diese Taste gedrückt gehalten wird, während ein Token gezogen wird, wird die Wegfindung vorübergehend aktiviert/deaktiviert"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"allowPathfinding": {
|
||||
"name": "Wegfindung für Spieler erlauben",
|
||||
"hint": "Erlaubt es Spielern die Wegfindungs zu benutzen. Bitte beachte, dass die Wegfindung Wege durch unerkundeten Nebel des Kriegs und Ätherische Wände berechnen kann. Dies kann deinen Spielern Geheimnisse lüften, von denen sie noch nicht erfahren sollten."
|
||||
},
|
||||
"alwaysShowSpeedForPCs": {
|
||||
"name": "Geschwindigkeit von Spielercharakteren für jeden anzeigen",
|
||||
"hint": "Wenn diese Einstellung aktiviert ist wird die Färbung der hervorgehobenen Felder bei Spielercharakteren allen Spielern angezeigt, selbst wenn diese keinen Zugriff auf den Charakterbogen haben."
|
||||
|
||||
@@ -40,9 +40,17 @@
|
||||
"moveWithoutAnimation": {
|
||||
"name": "Disable token animation",
|
||||
"hint": "When being held while dropping a token, the token will move to the target location without animating"
|
||||
},
|
||||
"togglePathfinding": {
|
||||
"name": "Toggle pathfinding",
|
||||
"hint": "When being held while dragging a token, the pathfinding functionality will be temporarily enabled/disabled"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"allowPathfinding": {
|
||||
"name": "Allow pathfinding for players",
|
||||
"hint": "Allows players to use Drag Ruler's pathfinding functionality in this world. Be aware that pathfinding can route through unexplored fog of war and Ethereal Walls, which might reveal secrets to your players ahead of time."
|
||||
},
|
||||
"alwaysShowSpeedForPCs": {
|
||||
"name": "Show PC speed to everyone",
|
||||
"hint": "If enabled the coloring based on actor speed for player characters will shown to everyone, even if they don't have observer permission for the character sheet."
|
||||
@@ -51,6 +59,10 @@
|
||||
"name": "Automatically start measuring",
|
||||
"hint": "If enabled, Drag Ruler will start measuring as soon as the token is being dragged. If disabled, Drag Ruler will remain inactive and will only start measuring once the button to add a waypoint is being pressed."
|
||||
},
|
||||
"autoPathfinding": {
|
||||
"name": "Pathfinding by default",
|
||||
"hint": "If enabled, dragging a token will automatically use a pathfinding ruler"
|
||||
},
|
||||
"enableMovementHistory": {
|
||||
"name": "Enable movement history during combat",
|
||||
"hint": "If enabled, Drag Ruler will remember the path a token took during it's turn in combat and will display it when you pick the token back up."
|
||||
|
||||
@@ -44,9 +44,17 @@
|
||||
"moveWithoutAnimation": {
|
||||
"name": "Deshabilitar animación de icono",
|
||||
"hint": "Si al soltar un icono se presiona esta tecla, se deshabilitará la animación del icono al moverse al destino"
|
||||
},
|
||||
"togglePathfinding": {
|
||||
"name": "Conmutar búsqueda de camino",
|
||||
"hint": "Cuando se presione al arrastrar un icono, la funcionalidad de búsqueda de camino será temporalmente habilitada/deshabilitada"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"allowPathfinding": {
|
||||
"name": "Permitir búsqueda de camino a los jugadores",
|
||||
"hint": "Permite a los jugadores usar en este mundo la funcionalidad de búsqueda de camino. Tenga cuidado porque la ruta puede transcurrir por lugares con niebla de guerra o muros invisibles, lo que podrá revelar algunos secretos a los jugadores antes de tiempo"
|
||||
},
|
||||
"alwaysShowSpeedForPCs": {
|
||||
"name": "Mostrar velocidad de los PJs a todo el mundo",
|
||||
"hint": "Si se habilita, se mostrará a todo el mundo los códigos de colores de las rutas de los PJs, incluso si no tienen permisos de observador para ese personaje"
|
||||
@@ -55,6 +63,10 @@
|
||||
"name": "Comenzar a medir automáticamente",
|
||||
"hint": "Si se habilita, Drag Ruler comenzará a medir en cuanto se comience a arrastrar un icono. Si se deshabilita, Drag Ruler permanecerá inactivo y comenzará a medir únicamente cuando se presione el botón configurado para añadir un nuevo punto de ruta"
|
||||
},
|
||||
"autoPathfinding": {
|
||||
"name": "Búsqueda de camino por defecto",
|
||||
"hint": "Si se habilita, al arrastrar un icono se usará automáticamente la regla de búsqueda de camino"
|
||||
},
|
||||
"enableMovementHistory": {
|
||||
"name": "Habilitar historial de movimiento durante el combate",
|
||||
"hint": "Si se habilita, Drag Ruler recordará la ruta que ha seguido un icono en su turno y la mostrará al seleccionarlo de nuevo"
|
||||
|
||||
+16
-11
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"name": "drag-ruler",
|
||||
"id": "drag-ruler",
|
||||
"title": "Drag Ruler",
|
||||
"description": "When dragging a token displays a ruler showing how far you've moved that token.",
|
||||
"version": "1.10.2",
|
||||
"minimumCoreVersion" : "9.238",
|
||||
"compatibleCoreVersion" : "9",
|
||||
"version": "1.13.0",
|
||||
"compatibility": {
|
||||
"minimum": "10",
|
||||
"verified": "10"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Manuel Vögele",
|
||||
@@ -57,15 +59,18 @@
|
||||
"path": "lang/zh-tw.json"
|
||||
}
|
||||
],
|
||||
"dependencies": [
|
||||
{
|
||||
"name": "socketlib",
|
||||
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-socketlib/master/module.json"
|
||||
}
|
||||
],
|
||||
"relationships": {
|
||||
"requires": [
|
||||
{
|
||||
"id": "socketlib",
|
||||
"type": "module",
|
||||
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-socketlib/master/module.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"socket": true,
|
||||
"url": "https://github.com/manuelVo/foundryvtt-drag-ruler",
|
||||
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/v1.10.2.zip",
|
||||
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/v1.13.0.zip",
|
||||
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/master/module.json",
|
||||
"readme": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/README.md",
|
||||
"changelog": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/CHANGELOG.md",
|
||||
|
||||
+107
-90
@@ -1,181 +1,198 @@
|
||||
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";
|
||||
import {GenericSpeedProvider, SpeedProvider} from "./speed_provider.js";
|
||||
import {settingsKey} from "./settings.js";
|
||||
import {getAreaFromPositionAndShape, getTokenShape} from "./util.js";
|
||||
|
||||
export const availableSpeedProviders = {}
|
||||
export let currentSpeedProvider = undefined
|
||||
export const availableSpeedProviders = {};
|
||||
export let currentSpeedProvider = undefined;
|
||||
|
||||
function register(module, type, speedProvider) {
|
||||
const id = `${type}.${module.id}`
|
||||
let providerInstance
|
||||
const id = `${type}.${module.id}`;
|
||||
let providerInstance;
|
||||
if (speedProvider.prototype instanceof SpeedProvider) {
|
||||
providerInstance = new speedProvider(id)
|
||||
providerInstance = new speedProvider(id);
|
||||
} else {
|
||||
console.warn(
|
||||
`Drag Ruler | The ${type} '${module.id}' uses the old, deprecated version of the Drag Ruler API. ` +
|
||||
"That old API will be removed in a future Drag Ruler version. " +
|
||||
`Please update the ${type} ${module.id} to stay compatible with future Drag Ruler versions.`,
|
||||
);
|
||||
speedProvider.id = id;
|
||||
speedProvider.usesRuler = () => true;
|
||||
providerInstance = speedProvider;
|
||||
}
|
||||
else {
|
||||
console.warn(`Drag Ruler | The ${type} '${module.id}' uses the old, deprecated version of the Drag Ruler API. ` +
|
||||
"That old API will be removed in a future Drag Ruler version. " +
|
||||
`Please update the ${type} ${module.id} to stay compatible with future Drag Ruler versions.`);
|
||||
speedProvider.id = id
|
||||
speedProvider.usesRuler = () => true
|
||||
providerInstance = speedProvider
|
||||
}
|
||||
setupProvider(providerInstance)
|
||||
setupProvider(providerInstance);
|
||||
}
|
||||
|
||||
function setupProvider(speedProvider) {
|
||||
if (speedProvider instanceof SpeedProvider) {
|
||||
const unreachableColor = {id: "unreachable", default: speedProvider.defaultUnreachableColor, name: "drag-ruler.settings.speedProviderSettings.color.unreachable.name"}
|
||||
const unreachableColor = {
|
||||
id: "unreachable",
|
||||
default: speedProvider.defaultUnreachableColor,
|
||||
name: "drag-ruler.settings.speedProviderSettings.color.unreachable.name",
|
||||
};
|
||||
for (const color of speedProvider.colors.concat([unreachableColor])) {
|
||||
game.settings.register(settingsKey, `speedProviders.${speedProvider.id}.color.${color.id}`, {
|
||||
config: false,
|
||||
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)
|
||||
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()
|
||||
availableSpeedProviders[speedProvider.id] = speedProvider;
|
||||
game.settings.settings.get("drag-ruler.speedProvider").default = getDefaultSpeedProvider();
|
||||
updateSpeedProvider();
|
||||
}
|
||||
|
||||
export function getDefaultSpeedProvider() {
|
||||
const providerIds = Object.keys(availableSpeedProviders)
|
||||
const providerIds = Object.keys(availableSpeedProviders);
|
||||
// Game systems take the highest precedence for the being the default
|
||||
const gameSystem = providerIds.find(key => key.startsWith("system."))
|
||||
if (gameSystem)
|
||||
return gameSystem
|
||||
const gameSystem = providerIds.find(key => key.startsWith("system."));
|
||||
if (gameSystem) return gameSystem;
|
||||
|
||||
// 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
|
||||
// settingKeys should always be sorted the same way so this should achive a stable default
|
||||
const module = providerIds.find(key => key.startsWith("module."))
|
||||
if (module)
|
||||
return module
|
||||
const module = providerIds.find(key => key.startsWith("module."));
|
||||
if (module) return module;
|
||||
|
||||
// If neither a game system or a module is found fall back to the native implementation
|
||||
return providerIds[0]
|
||||
return providerIds[0];
|
||||
}
|
||||
|
||||
export function updateSpeedProvider() {
|
||||
// If the configured provider is registered use that one. If not use the default provider
|
||||
const configuredProvider = game.settings.get("drag-ruler", "speedProvider")
|
||||
currentSpeedProvider = availableSpeedProviders[configuredProvider] ?? availableSpeedProviders[game.settings.settings.get("drag-ruler.speedProvider").default]
|
||||
const configuredProvider = game.settings.get("drag-ruler", "speedProvider");
|
||||
currentSpeedProvider =
|
||||
availableSpeedProviders[configuredProvider] ??
|
||||
availableSpeedProviders[game.settings.settings.get("drag-ruler.speedProvider").default];
|
||||
}
|
||||
|
||||
export function initApi() {
|
||||
const genericSpeedProviderInstance = new GenericSpeedProvider("native")
|
||||
setupProvider(genericSpeedProviderInstance)
|
||||
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)
|
||||
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}`)
|
||||
range.color = game.settings.get(
|
||||
settingsKey,
|
||||
`speedProviders.${currentSpeedProvider.id}.color.${range.color}`,
|
||||
);
|
||||
}
|
||||
return ranges
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
return []
|
||||
return ranges;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function getUnreachableColorFromSpeedProvider() {
|
||||
if (currentSpeedProvider instanceof Function)
|
||||
return 0xFF0000
|
||||
if (currentSpeedProvider instanceof Function) return 0xff0000;
|
||||
try {
|
||||
return game.settings.get(settingsKey, `speedProviders.${currentSpeedProvider.id}.color.unreachable`)
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
return 0xFF0000
|
||||
}
|
||||
}
|
||||
|
||||
export function getCostFromSpeedProvider(token, area, options) {
|
||||
try {
|
||||
if (currentSpeedProvider instanceof Function) {
|
||||
return SpeedProvider.prototype.getCostForStep.call(undefined, token, area, options);
|
||||
}
|
||||
return currentSpeedProvider.getCostForStep(token, area, options);
|
||||
}
|
||||
catch (e) {
|
||||
return game.settings.get(
|
||||
settingsKey,
|
||||
`speedProviders.${currentSpeedProvider.id}.color.unreachable`,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return 1;
|
||||
return 0xff0000;
|
||||
}
|
||||
}
|
||||
|
||||
export function getColorForDistanceAndToken(distance, token, ranges=null) {
|
||||
export function getColorForDistanceAndToken(distance, token, ranges = null) {
|
||||
if (!ranges) {
|
||||
ranges = getRangesFromSpeedProvider(token);
|
||||
}
|
||||
if (ranges.length === 0)
|
||||
return null;
|
||||
const currentRange = ranges.reduce((minRange, currentRange) => {
|
||||
if (distance <= currentRange.range && currentRange.range < minRange.range)
|
||||
return currentRange;
|
||||
return minRange;
|
||||
}, {range: Infinity, color: getUnreachableColorFromSpeedProvider()});
|
||||
if (ranges.length === 0) return null;
|
||||
const currentRange = ranges.reduce(
|
||||
(minRange, currentRange) => {
|
||||
if (distance <= currentRange.range && currentRange.range < minRange.range)
|
||||
return currentRange;
|
||||
return minRange;
|
||||
},
|
||||
{range: Infinity, color: getUnreachableColorFromSpeedProvider()},
|
||||
);
|
||||
return currentRange.color;
|
||||
}
|
||||
|
||||
export function getMovedDistanceFromToken(token) {
|
||||
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active;
|
||||
const history = getMovementHistory(token);
|
||||
const segments = Ruler.dragRulerGetRaysFromWaypoints(history, {x: token.x, y: token.y}).map(ray => {return {ray}});
|
||||
const segments = CONFIG.Canvas.rulerClass
|
||||
.dragRulerGetRaysFromWaypoints(history, {x: token.x, y: token.y})
|
||||
.map(ray => {
|
||||
return {ray};
|
||||
});
|
||||
const shape = getTokenShape(token);
|
||||
const distances = measureDistances(segments, token, shape, {enableTerrainRuler: terrainRulerAvailable});
|
||||
const distances = measureDistances(segments, token, shape, {
|
||||
enableTerrainRuler: terrainRulerAvailable,
|
||||
});
|
||||
// Sum up the distances
|
||||
return distances.reduce((acc, val) => acc + val, 0);
|
||||
}
|
||||
|
||||
export function buildCostFunction(token, shape) {
|
||||
return (x, y, costOptions = {}) => {
|
||||
costOptions.token = token;
|
||||
const area = getAreaFromPositionAndShape({x, y}, shape);
|
||||
const costs = area.map(space => canvas.terrain.cost({x: space.x, y: space.y, costOptions}));
|
||||
return costs.reduce((max, current) => Math.max(max, current));
|
||||
};
|
||||
}
|
||||
|
||||
export function registerModule(moduleId, speedProvider) {
|
||||
// Check if a module with the given id exists and is currently enabled
|
||||
const module = game.modules.get(moduleId)
|
||||
const module = game.modules.get(moduleId);
|
||||
// If it doesn't the calling module did something wrong. Log a warning and ignore this module
|
||||
if (!module) {
|
||||
console.warn(
|
||||
`Drag Ruler | A module tried to register with the id "${moduleId}". However no active module with this id was found.` +
|
||||
"This api registration call was ignored. " +
|
||||
"If you are the author of that module please check that the id passed to `registerModule` matches the id in your manifest exactly." +
|
||||
"If this call was made form a game system instead of a module please use `registerSystem` instead.")
|
||||
return
|
||||
"This api registration call was ignored. " +
|
||||
"If you are the author of that module please check that the id passed to `registerModule` matches the id in your manifest exactly." +
|
||||
"If this call was made form a game system instead of a module please use `registerSystem` instead.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Using Drag Ruler's id is not allowed
|
||||
if (moduleId === "drag-ruler") {
|
||||
console.warn(
|
||||
`Drag Ruler | A module tried to register with the id "${moduleId}", which is not allowed. This api registration call was ignored. ` +
|
||||
"If you're the author of the module please use the id of your own module as it's specified in your manifest to register to this api. " +
|
||||
"If this call was made form a game system instead of a module please use `registerSystem` instead."
|
||||
)
|
||||
return
|
||||
"If you're the author of the module please use the id of your own module as it's specified in your manifest to register to this api. " +
|
||||
"If this call was made form a game system instead of a module please use `registerSystem` instead.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
register(module, "module", speedProvider)
|
||||
register(module, "module", speedProvider);
|
||||
}
|
||||
|
||||
export function registerSystem(systemId, speedProvider) {
|
||||
const system = game.system
|
||||
const system = game.system;
|
||||
// If the current system id doesn't match the provided id something went wrong. Log a warning and ignore this module
|
||||
if (system.id != systemId) {
|
||||
console.warn(
|
||||
`Drag Ruler | A system tried to register with the id "${systemId}". However the active system has a different id.` +
|
||||
"This api registration call was ignored. " +
|
||||
"If you are the author of that system please check that the id passed to `registerSystem` matches the id in your manifest exactly." +
|
||||
"If this call was made form a module instead of a game system please use `registerModule` instead.")
|
||||
return
|
||||
"This api registration call was ignored. " +
|
||||
"If you are the author of that system please check that the id passed to `registerSystem` matches the id in your manifest exactly." +
|
||||
"If this call was made form a module instead of a game system please use `registerModule` instead.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
register(system, "system", speedProvider)
|
||||
register(system, "system", speedProvider);
|
||||
}
|
||||
|
||||
+50
-26
@@ -1,21 +1,26 @@
|
||||
import {getCostFromSpeedProvider} from "./api.js";
|
||||
import {buildCostFunction} from "./api.js";
|
||||
import {settingsKey} from "./settings.js";
|
||||
import {getAreaFromPositionAndShape, highlightTokenShape} from "./util.js";
|
||||
import {highlightTokenShape} from "./util.js";
|
||||
|
||||
export function getHexSizeSupportTokenGridCenter(token) {
|
||||
const tokenCenterOffset = CONFIG.hexSizeSupport.getCenterOffset(token)
|
||||
return {x: token.x + tokenCenterOffset.x, y: token.y + tokenCenterOffset.y}
|
||||
const tokenCenterOffset = CONFIG.hexSizeSupport.getCenterOffset(token);
|
||||
return {x: token.x + tokenCenterOffset.x, y: token.y + tokenCenterOffset.y};
|
||||
}
|
||||
|
||||
export function highlightMeasurementTerrainRuler(ray, startDistance, tokenShape=[{x: 0, y: 0}], alpha=1) {
|
||||
export function highlightMeasurementTerrainRuler(
|
||||
ray,
|
||||
startDistance,
|
||||
tokenShape = [{x: 0, y: 0}],
|
||||
alpha = 1,
|
||||
) {
|
||||
for (const space of ray.terrainRulerVisitedSpaces.reverse()) {
|
||||
const color = this.dragRulerGetColorForDistance(startDistance + space.distance);
|
||||
highlightTokenShape.call(this, space, tokenShape, color, alpha)
|
||||
highlightTokenShape.call(this, space, tokenShape, color, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
export function measureDistances(segments, entity, shape, options={}) {
|
||||
const opts = duplicate(options)
|
||||
export function measureDistances(segments, entity, shape, options = {}) {
|
||||
const opts = duplicate(options);
|
||||
if (canvas.grid.diagonalRule === "EUCL") {
|
||||
opts.ignoreGrid = true;
|
||||
opts.gridSpaes = false;
|
||||
@@ -25,17 +30,23 @@ export function measureDistances(segments, entity, shape, options={}) {
|
||||
const firstNewSegmentIndex = segments.findIndex(segment => !segment.ray.dragRulerVisitedSpaces);
|
||||
const previousSegments = segments.slice(0, firstNewSegmentIndex);
|
||||
const newSegments = segments.slice(firstNewSegmentIndex);
|
||||
const distances = previousSegments.map(segment => segment.ray.dragRulerVisitedSpaces[segment.ray.dragRulerVisitedSpaces.length - 1].distance);
|
||||
previousSegments.forEach(segment => segment.ray.terrainRulerVisitedSpaces = duplicate(segment.ray.dragRulerVisitedSpaces));
|
||||
opts.costFunction = (x, y, costOptions={}) => { return getCostFromSpeedProvider(entity, getAreaFromPositionAndShape({x, y}, shape), costOptions); }
|
||||
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 = buildCostFunction(entity, shape);
|
||||
if (previousSegments.length > 0)
|
||||
opts.terrainRulerInitialState = previousSegments[previousSegments.length - 1].ray.dragRulerFinalState;
|
||||
opts.terrainRulerInitialState =
|
||||
previousSegments[previousSegments.length - 1].ray.dragRulerFinalState;
|
||||
return distances.concat(terrainRuler.measureDistances(newSegments, opts));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// If another module wants to enable grid measurements but disable grid highlighting,
|
||||
// manually set the *duplicate* option's gridSpaces value to true for the Foundry logic to work properly
|
||||
if(!opts.ignoreGrid) {
|
||||
if (!opts.ignoreGrid) {
|
||||
opts.gridSpaces = true;
|
||||
}
|
||||
return canvas.grid.measureDistances(segments, opts);
|
||||
@@ -44,23 +55,31 @@ export function measureDistances(segments, entity, shape, options={}) {
|
||||
|
||||
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.");
|
||||
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>`,
|
||||
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")
|
||||
}
|
||||
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")) {
|
||||
} 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
|
||||
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;
|
||||
@@ -68,20 +87,25 @@ export function checkDependencies() {
|
||||
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>`,
|
||||
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()),
|
||||
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())
|
||||
close: () => game.settings.set(settingsKey, "lastTerrainRulerHintTime", Date.now()),
|
||||
}).render(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* A combination queue/set where the elements are ordered (in ascending order, according to the given priority function)
|
||||
* and unique (according to the given elementMatcher).
|
||||
*
|
||||
* If an element is added to the set and an equivalent element already exists, the lower-priority one is discarded.
|
||||
*/
|
||||
export class PriorityQueueSet {
|
||||
constructor(elementMatcher, priorityFunction) {
|
||||
this.first = null;
|
||||
this.elementMatcher = elementMatcher;
|
||||
this.priorityFunction = priorityFunction;
|
||||
}
|
||||
|
||||
pushWithPriority(value) {
|
||||
const newNode = {value, priority: this.priorityFunction(value), next: null};
|
||||
|
||||
// If the queue is currently empty, we can just set this new node as the first and we're done
|
||||
if (!this.first) {
|
||||
this.first = newNode;
|
||||
return;
|
||||
}
|
||||
|
||||
let inserted = false;
|
||||
let previous;
|
||||
let current = this.first;
|
||||
|
||||
// Loop through the existing elements
|
||||
while (current) {
|
||||
if (this.elementMatcher(current.value, value)) {
|
||||
// We've found an equivalent element before one with a lower priority. This one has at least
|
||||
// the same priority as the new one, so don't bother inserting
|
||||
return;
|
||||
} else if (newNode.priority <= current.priority) {
|
||||
// We've found some element with lower priority than the new one, so insert the new one just before it
|
||||
newNode.next = current;
|
||||
if (previous) {
|
||||
previous.next = newNode;
|
||||
} else {
|
||||
this.first = newNode;
|
||||
}
|
||||
inserted = true;
|
||||
|
||||
previous = current;
|
||||
current = current.next;
|
||||
break;
|
||||
}
|
||||
previous = current;
|
||||
current = current.next;
|
||||
}
|
||||
|
||||
if (inserted) {
|
||||
// Go through the rest of the list and try to find an equivalent element to the new one.
|
||||
// We know it has higher priority than the new one, so remove it.
|
||||
while (current) {
|
||||
if (this.elementMatcher(current.value, value)) {
|
||||
if (previous) {
|
||||
previous.next = current.next;
|
||||
} else {
|
||||
this.first = current.next;
|
||||
}
|
||||
return;
|
||||
}
|
||||
previous = current;
|
||||
current = current.next;
|
||||
}
|
||||
} else {
|
||||
// We reached the end of the queue without finding a lower-priority or existing element, so
|
||||
// insert the new one at the end
|
||||
previous.next = newNode;
|
||||
}
|
||||
}
|
||||
|
||||
hasNext() {
|
||||
return !!this.first;
|
||||
}
|
||||
|
||||
pop() {
|
||||
const first = this.first;
|
||||
this.first = first?.next;
|
||||
return first?.value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue that will only ever accept elements with a given value once. Elements must have a "value" field, the
|
||||
* JSON representation of which will be used as the key to match
|
||||
*/
|
||||
export class ProcessOnceQueue {
|
||||
constructor() {
|
||||
this.first = null;
|
||||
this.last = null;
|
||||
this.previouslyQueued = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove everything from the queue and forget all the previously-queued items
|
||||
*/
|
||||
reset() {
|
||||
this.first = null;
|
||||
this.last = null;
|
||||
this.previouslyQueued.clear();
|
||||
}
|
||||
|
||||
push(element) {
|
||||
if (this.previouslyQueued.has(element)) {
|
||||
return;
|
||||
}
|
||||
this.previouslyQueued.add(element);
|
||||
|
||||
const newNode = {
|
||||
value: element,
|
||||
next: null,
|
||||
previous: null,
|
||||
};
|
||||
|
||||
if (!this.first) {
|
||||
this.first = newNode;
|
||||
this.last = newNode;
|
||||
} else {
|
||||
this.last.next = newNode;
|
||||
newNode.previous = this.last;
|
||||
this.last = newNode;
|
||||
}
|
||||
}
|
||||
|
||||
pop() {
|
||||
const node = this.first;
|
||||
this.first = node?.next;
|
||||
if (!node?.next) {
|
||||
this.last = null;
|
||||
}
|
||||
return node?.value;
|
||||
}
|
||||
|
||||
hasNext() {
|
||||
return !!this.first;
|
||||
}
|
||||
}
|
||||
+23
-6
@@ -3,16 +3,33 @@
|
||||
// https://gitlab.com/foundrynet/foundryvtt/-/issues/4705
|
||||
export function getPixelsFromGridPosition(xGrid, yGrid) {
|
||||
if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS) {
|
||||
return canvas.grid.grid.getPixelsFromGridPosition(yGrid, xGrid)
|
||||
return canvas.grid.grid.getPixelsFromGridPosition(yGrid, xGrid);
|
||||
}
|
||||
return canvas.grid.grid.getPixelsFromGridPosition(xGrid, yGrid)
|
||||
return canvas.grid.grid.getPixelsFromGridPosition(xGrid, yGrid);
|
||||
}
|
||||
|
||||
// Wrapper to fix a FoundryVTT bug that causes the return values of canvas.grid.grid.getPixelsFromGridPosition to be ordered inconsistently
|
||||
// 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]
|
||||
const [x, y] = canvas.grid.grid.getGridPositionFromPixels(xPixel, yPixel);
|
||||
if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS) return [y, x];
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
export function getGridPositionFromPixelsObj(o) {
|
||||
const r = {};
|
||||
[r.x, r.y] = getGridPositionFromPixels(o.x, o.y);
|
||||
return r;
|
||||
}
|
||||
|
||||
export function getPixelsFromGridPositionObj(o) {
|
||||
const r = {};
|
||||
[r.x, r.y] = getPixelsFromGridPosition(o.x, o.y);
|
||||
return r;
|
||||
}
|
||||
|
||||
export function getCenterFromGridPositionObj(o) {
|
||||
const r = getPixelsFromGridPositionObj(o);
|
||||
[r.x, r.y] = canvas.grid.getCenter(r.x, r.y);
|
||||
return r;
|
||||
}
|
||||
|
||||
+75
-175
@@ -1,10 +1,8 @@
|
||||
import {highlightMeasurementTerrainRuler, measureDistances} from "./compatibility.js";
|
||||
import {getGridPositionFromPixels} from "./foundry_fixes.js";
|
||||
import {Line} from "./geometry.js";
|
||||
import {disableSnap, moveWithoutAnimation} from "./keybindings.js";
|
||||
import {trackRays} from "./movement_tracking.js"
|
||||
import {trackRays} from "./movement_tracking.js";
|
||||
import {recalculate} from "./socket.js";
|
||||
import {applyTokenSizeOffset, enumeratedZip, getSnapPointForEntity, getSnapPointForToken, getTokenShape, highlightTokenShape, sum} from "./util.js";
|
||||
import {getSnapPointForToken, highlightTokenShape, sum} from "./util.js";
|
||||
|
||||
// This is a modified version of Ruler.moveToken from foundry 0.7.9
|
||||
export async function moveEntities(draggedEntity, selectedEntities) {
|
||||
@@ -25,9 +23,17 @@ export async function moveEntities(draggedEntity, selectedEntities) {
|
||||
if (!game.user.isGM && draggedEntity instanceof Token) {
|
||||
const hasCollision = selectedEntities.some(token => {
|
||||
const offset = calculateEntityOffset(token, draggedEntity);
|
||||
const offsetRays = rays.filter(ray => !ray.isPrevious).map(ray => applyOffsetToRay(ray, offset))
|
||||
return offsetRays.some(r => canvas.walls.checkCollision(r));
|
||||
})
|
||||
const offsetRays = rays
|
||||
.filter(ray => !ray.isPrevious)
|
||||
.map(ray => applyOffsetToRay(ray, offset));
|
||||
return offsetRays.some(r =>
|
||||
token.checkCollision(r.B, {
|
||||
origin: r.A,
|
||||
mode: "any",
|
||||
type: "move",
|
||||
}),
|
||||
);
|
||||
});
|
||||
if (hasCollision) {
|
||||
ui.notifications.error(game.i18n.localize("ERROR.TokenCollide"));
|
||||
this._endMeasurement();
|
||||
@@ -41,8 +47,7 @@ export async function moveEntities(draggedEntity, selectedEntities) {
|
||||
await animateEntities.call(this, selectedEntities, draggedEntity, rays, wasPaused);
|
||||
|
||||
// Once all animations are complete we can clear the ruler
|
||||
if (this.draggedEntity?.id === draggedEntity.id)
|
||||
this._endMeasurement();
|
||||
if (this.draggedEntity?.id === draggedEntity.id) this._endMeasurement();
|
||||
}
|
||||
|
||||
// This is a modified version code extracted from Ruler.moveToken from foundry 0.7.9
|
||||
@@ -58,12 +63,11 @@ async function animateEntities(entities, draggedEntity, draggedRays, wasPaused)
|
||||
const origin = [firstWaypoint.x + entityOffset.x, firstWaypoint.y + entityOffset.y];
|
||||
let dx, dy;
|
||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||
dx = entity.data.x - origin[0];
|
||||
dy = entity.data.y - origin[1];
|
||||
}
|
||||
else {
|
||||
dx = entity.data.x - origin[0];
|
||||
dy = entity.data.y - origin[1];
|
||||
dx = entity.x - origin[0];
|
||||
dy = entity.y - origin[1];
|
||||
} else {
|
||||
dx = entity.x - origin[0];
|
||||
dy = entity.y - origin[1];
|
||||
}
|
||||
|
||||
return {entity, rays: offsetRays, dx, dy};
|
||||
@@ -76,7 +80,7 @@ async function animateEntities(entities, draggedEntity, draggedRays, wasPaused)
|
||||
// This is a flag of the "Monk's Active Tile Triggers" module that signals that the movement should be cancelled early
|
||||
this.cancelMovement = false;
|
||||
|
||||
for (let i = startWaypoint;i < entityAnimationData[0].rays.length; i++) {
|
||||
for (let i = startWaypoint; i < entityAnimationData[0].rays.length; i++) {
|
||||
if (!wasPaused && game.paused) break;
|
||||
const entityPaths = entityAnimationData.map(({entity, rays, dx, dy}) => {
|
||||
const ray = rays[i];
|
||||
@@ -87,26 +91,38 @@ async function animateEntities(entities, draggedEntity, draggedRays, wasPaused)
|
||||
const updates = entityPaths.map(({entity, path}) => {
|
||||
return {x: path.B.x, y: path.B.y, _id: entity.id};
|
||||
});
|
||||
await draggedEntity.scene.updateEmbeddedDocuments(draggedEntity.constructor.embeddedName, updates, {animate});
|
||||
await draggedEntity.scene.updateEmbeddedDocuments(
|
||||
draggedEntity.constructor.embeddedName,
|
||||
updates,
|
||||
{animate},
|
||||
);
|
||||
if (animate)
|
||||
await Promise.all(entityPaths.map(({entity, path}) => entity.animateMovement(path)));
|
||||
await Promise.all(
|
||||
entityPaths.map(({entity}) => CanvasAnimation.getAnimation(entity.animationName)?.promise),
|
||||
);
|
||||
|
||||
// This is a flag of the "Monk's Active Tile Triggers" module that signals that the movement should be cancelled early
|
||||
if (this.cancelMovement) {
|
||||
entityAnimationData.forEach(ead => ead.rays = ead.rays.slice(0, i + 1));
|
||||
entityAnimationData.forEach(ead => (ead.rays = ead.rays.slice(0, i + 1)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isToken)
|
||||
trackRays(entities, entityAnimationData.map(({rays}) => rays)).then(() => recalculate(entities));
|
||||
trackRays(
|
||||
entities,
|
||||
entityAnimationData.map(({rays}) => rays),
|
||||
).then(() => recalculate(entities));
|
||||
}
|
||||
|
||||
function calculateEntityOffset(entityA, entityB) {
|
||||
return {x: entityA.data.x - entityB.data.x, y: entityA.data.y - entityB.data.y};
|
||||
return {x: entityA.x - entityB.x, y: entityA.y - entityB.y};
|
||||
}
|
||||
|
||||
function applyOffsetToRay(ray, offset) {
|
||||
const newRay = new Ray({x: ray.A.x + offset.x, y: ray.A.y + offset.y}, {x: ray.B.x + offset.x, y: ray.B.y + offset.y});
|
||||
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;
|
||||
}
|
||||
@@ -116,7 +132,10 @@ export function onMouseMove(event) {
|
||||
if (this._state === Ruler.STATES.MOVING) return;
|
||||
|
||||
// Extract event data
|
||||
const destination = {x: event.data.destination.x + this.rulerOffset.x, y: event.data.destination.y + this.rulerOffset.y}
|
||||
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();
|
||||
@@ -135,12 +154,21 @@ function scheduleMeasurement(destination, event) {
|
||||
event._measureTime = Date.now();
|
||||
this._state = Ruler.STATES.MEASURING;
|
||||
cancelScheduledMeasurement.call(this);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.deferredMeasurementData = {destination, event};
|
||||
if (!this.deferredMeasurementTimeout) {
|
||||
this.deferredMeasurementPromise = new Promise((resolve, reject) => this.deferredMeasurementResolve = resolve);
|
||||
this.deferredMeasurementTimeout = window.setTimeout(() => scheduleMeasurement.call(this, this.deferredMeasurementData.destination, this.deferredMeasurementData.event), measurementInterval);
|
||||
this.deferredMeasurementPromise = new Promise(
|
||||
(resolve, reject) => (this.deferredMeasurementResolve = resolve),
|
||||
);
|
||||
this.deferredMeasurementTimeout = window.setTimeout(
|
||||
() =>
|
||||
scheduleMeasurement.call(
|
||||
this,
|
||||
this.deferredMeasurementData.destination,
|
||||
this.deferredMeasurementData.event,
|
||||
),
|
||||
measurementInterval,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,163 +179,34 @@ export function cancelScheduledMeasurement() {
|
||||
this.deferredMeasurementResolve?.();
|
||||
}
|
||||
|
||||
// This is a modified version of Ruler.measure form foundry 0.7.9
|
||||
export function measure(destination, options={}) {
|
||||
const isToken = this.draggedEntity instanceof Token;
|
||||
if (isToken && !this.draggedEntity.isVisible)
|
||||
return []
|
||||
|
||||
if (options.snap) {
|
||||
destination = getSnapPointForEntity(destination.x, destination.y, this.draggedEntity);
|
||||
}
|
||||
|
||||
if(options.gridSpaces === undefined) {
|
||||
options.gridSpaces = canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS;
|
||||
}
|
||||
|
||||
if (canvas.grid.diagonalRule === "EUCL") {
|
||||
options.gridSpaces = false;
|
||||
options.ignoreGrid = true;
|
||||
}
|
||||
|
||||
if(options.ignoreGrid === undefined) {
|
||||
options.ignoreGrid = false;
|
||||
}
|
||||
|
||||
options.enableTerrainRuler = isToken && game.modules.get("terrain-ruler")?.active;
|
||||
|
||||
const waypoints = this.waypoints.concat([destination]);
|
||||
// Move the waypoints to the center of the grid if a size is used that measures from edge to edge
|
||||
const centeredWaypoints = isToken ? applyTokenSizeOffset(waypoints, this.draggedEntity) : duplicate(waypoints);
|
||||
// Foundries native ruler requires the waypoints to sit in the dead center of the square to work properly
|
||||
if (!options.enableTerrainRuler && !options.ignoreGrid)
|
||||
centeredWaypoints.forEach(w => [w.x, w.y] = canvas.grid.getCenter(w.x, w.y));
|
||||
|
||||
const r = this.ruler;
|
||||
this.destination = destination;
|
||||
|
||||
// Iterate over waypoints and construct segment rays
|
||||
const segments = [];
|
||||
const centeredSegments = []
|
||||
for (let [i, dest] of waypoints.slice(1).entries()) {
|
||||
const centeredDest = centeredWaypoints[i + 1]
|
||||
const origin = waypoints[i];
|
||||
const centeredOrigin = centeredWaypoints[i]
|
||||
const label = this.labels.children[i];
|
||||
const ray = new Ray(origin, dest);
|
||||
const centeredRay = new Ray(centeredOrigin, centeredDest)
|
||||
ray.isPrevious = Boolean(origin.isPrevious);
|
||||
centeredRay.isPrevious = ray.isPrevious;
|
||||
ray.dragRulerVisitedSpaces = origin.dragRulerVisitedSpaces;
|
||||
centeredRay.dragRulerVisitedSpaces = ray.dragRulerVisitedSpaces;
|
||||
ray.dragRulerFinalState = origin.dragRulerFinalState;
|
||||
centeredRay.dragRulerFinalState = ray.dragRulerFinalState;
|
||||
if (ray.distance < 10) {
|
||||
if (label) label.visible = false;
|
||||
continue;
|
||||
}
|
||||
segments.push({ ray, label });
|
||||
centeredSegments.push({ray: centeredRay, label})
|
||||
}
|
||||
|
||||
|
||||
const shape = isToken ? getTokenShape(this.draggedEntity) : null;
|
||||
|
||||
// Compute measured distance
|
||||
const distances = measureDistances(centeredSegments, this.draggedEntity, shape, options);
|
||||
|
||||
let totalDistance = 0;
|
||||
for (let [i, d] of distances.entries()) {
|
||||
let s = centeredSegments[i];
|
||||
s.startDistance = totalDistance
|
||||
totalDistance += d;
|
||||
s.last = i === (centeredSegments.length - 1);
|
||||
s.distance = d;
|
||||
s.text = this._getSegmentLabel(d, totalDistance, s.last);
|
||||
}
|
||||
|
||||
// Clear the grid highlight layer
|
||||
const hlt = canvas.grid.highlightLayers[this.name] || canvas.grid.addHighlightLayer(this.name);
|
||||
hlt.clear();
|
||||
|
||||
// Draw measured path
|
||||
r.clear();
|
||||
let rulerColor
|
||||
if (!options.gridSpaces || canvas.grid.type === CONST.GRID_TYPES.GRIDLESS)
|
||||
rulerColor = this.dragRulerGetColorForDistance(totalDistance);
|
||||
else
|
||||
rulerColor = this.color
|
||||
for (const [i, s, cs] of enumeratedZip([...segments].reverse(), [...centeredSegments].reverse())) {
|
||||
const { label, text, last } = cs;
|
||||
|
||||
// Draw line segment
|
||||
const opacityMultiplier = s.ray.isPrevious ? 0.33 : 1;
|
||||
r.lineStyle(6, 0x000000, 0.5 * opacityMultiplier).moveTo(s.ray.A.x, s.ray.A.y).lineTo(s.ray.B.x, s.ray.B.y)
|
||||
.lineStyle(4, rulerColor, 0.25 * opacityMultiplier).moveTo(s.ray.A.x, s.ray.A.y).lineTo(s.ray.B.x, s.ray.B.y);
|
||||
|
||||
// Draw the distance label just after the endpoint of the segment
|
||||
if (label) {
|
||||
label.text = text;
|
||||
label.alpha = last ? 1.0 : 0.5;
|
||||
label.visible = true;
|
||||
let labelPosition = {x: s.ray.x0, y: s.ray.y0};
|
||||
labelPosition.x -= label.width / 2;
|
||||
labelPosition.y -= label.height / 2;
|
||||
const rayLine = Line.fromPoints(s.ray.A, s.ray.B);
|
||||
const rayLabelXHitY = rayLine.calcY(labelPosition.x);
|
||||
let innerDistance;
|
||||
// If ray hits top or bottom side of label
|
||||
if (rayLine.isVertical || rayLabelXHitY < labelPosition.y || rayLabelXHitY > labelPosition.y + label.height)
|
||||
innerDistance = Math.abs((label.height / 2) / Math.sin(s.ray.angle));
|
||||
// If ray hits left or right side of label
|
||||
else
|
||||
innerDistance = Math.abs((label.width / 2) / Math.cos(s.ray.angle));
|
||||
labelPosition = s.ray.project((s.ray.distance + 50 + innerDistance) / s.ray.distance);
|
||||
labelPosition.x -= label.width / 2;
|
||||
labelPosition.y -= label.height / 2;
|
||||
label.position.set(labelPosition.x, labelPosition.y);
|
||||
}
|
||||
|
||||
// Highlight grid positions
|
||||
if (isToken && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS && options.gridSpaces) {
|
||||
if (options.enableTerrainRuler) {
|
||||
highlightMeasurementTerrainRuler.call(this, cs.ray, cs.startDistance, shape, opacityMultiplier)
|
||||
}
|
||||
else {
|
||||
const previousSegments = centeredSegments.slice(0, segments.length - 1 - i);
|
||||
highlightMeasurementNative.call(this, cs.ray, previousSegments, shape, opacityMultiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw endpoints
|
||||
for (let p of waypoints) {
|
||||
r.lineStyle(2, 0x000000, 0.5).beginFill(rulerColor, 0.25).drawCircle(p.x, p.y, 8);
|
||||
}
|
||||
|
||||
// Return the measured segments
|
||||
return segments;
|
||||
}
|
||||
|
||||
export function highlightMeasurementNative(ray, previousSegments, tokenShape=[{x: 0, y: 0}], alpha=1) {
|
||||
const spacer = canvas.scene.data.gridType === CONST.GRID_TYPES.SQUARE ? 1.41 : 1;
|
||||
const nMax = Math.max(Math.floor(ray.distance / (spacer * Math.min(canvas.grid.w, canvas.grid.h))), 1);
|
||||
const tMax = Array.fromRange(nMax+1).map(t => t / nMax);
|
||||
export function highlightMeasurementNative(
|
||||
ray,
|
||||
previousSegments,
|
||||
tokenShape = [{x: 0, y: 0}],
|
||||
alpha = 1,
|
||||
) {
|
||||
const spacer = canvas.scene.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() ) {
|
||||
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 [x0, y0] = i === 0 ? [null, null] : prior;
|
||||
let [x1, y1] = canvas.grid.grid.getGridPositionFromPixels(x, y);
|
||||
if ( x0 === x1 && y0 === y1 ) continue;
|
||||
if (x0 === x1 && y0 === y1) continue;
|
||||
|
||||
// Highlight the grid position
|
||||
let [xg, yg] = canvas.grid.grid.getPixelsFromGridPosition(x1, y1);
|
||||
let [xgtl, ygtl] = canvas.grid.grid.getPixelsFromGridPosition(x1, y1);
|
||||
let [xg, yg] = canvas.grid.grid.getCenter(xgtl, ygtl);
|
||||
const pathUntilSpace = previousSegments.concat([{ray: new Ray(ray.A, {x: xg, y: yg})}]);
|
||||
const distance = sum(canvas.grid.measureDistances(pathUntilSpace, {gridSpaces: true}));
|
||||
const color = this.dragRulerGetColorForDistance(distance);
|
||||
@@ -318,10 +217,11 @@ export function highlightMeasurementNative(ray, previousSegments, tokenShape=[{x
|
||||
|
||||
// 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 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);
|
||||
let [xghtl, yghtl] = canvas.grid.grid.getPixelsFromGridPosition(x1h, y1h);
|
||||
let [xgh, ygh] = canvas.grid.grid.getCenter(xghtl, yghtl);
|
||||
const pathUntilSpace = previousSegments.concat([{ray: new Ray(ray.A, {x: xgh, y: ygh})}]);
|
||||
const distance = sum(canvas.grid.measureDistances(pathUntilSpace, {gridSpaces: true}));
|
||||
const color = this.dragRulerGetColorForDistance(distance);
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Functions taken directly from the hex-size-support module (https://github.com/Ourobor/Hex-Size-Support/releases/tag/1.1.0).
|
||||
* Unless otherwise stated, these functions are taken as-is.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Altered version of this function.
|
||||
* - Instead of taking a token as a parameter to retrieve the altOrientationFlag, receive the flag value directly
|
||||
* - Instead of taking a grid parameter, get the grid value from the globas canvas
|
||||
*/
|
||||
export function findVertexSnapPoint(x, y, altOrientationFlag) {
|
||||
const grid = canvas.grid.grid;
|
||||
if (grid.columns) {
|
||||
return findSnapPointCols(x, y, grid.h, grid.w, altOrientationFlag);
|
||||
} else {
|
||||
return findSnapPointRows(x, y, grid.h, grid.w, altOrientationFlag);
|
||||
}
|
||||
}
|
||||
|
||||
function findSnapPointRows(x, y, h, w, alt) {
|
||||
let xOffset = 0.0;
|
||||
if (canvas.grid.grid.even) {
|
||||
xOffset = -0.5;
|
||||
}
|
||||
|
||||
let yOffset1 = 0.75;
|
||||
let yOffset2 = 0.0;
|
||||
if (alt) {
|
||||
yOffset1 = 0.25;
|
||||
yOffset2 = 1.0;
|
||||
}
|
||||
|
||||
let row1 = calculateSnapPointsRows(x, y, h, w, 0.5 + xOffset, yOffset1);
|
||||
let row2 = calculateSnapPointsRows(x, y, h, w, 1.0 + xOffset, yOffset2);
|
||||
|
||||
let dist1 = Math.pow(row1.x - x, 2) + Math.pow(row1.y - y, 2);
|
||||
let dist2 = Math.pow(row2.x - x, 2) + Math.pow(row2.y - y, 2);
|
||||
|
||||
if (dist1 < dist2) {
|
||||
return row1;
|
||||
} else {
|
||||
return row2;
|
||||
}
|
||||
}
|
||||
|
||||
function calculateSnapPointsRows(x, y, h, w, xOff, yOff) {
|
||||
let c = Math.floor((x + (0.5 - xOff) * w) / w + 1);
|
||||
let r = Math.floor((y + (0.75 - yOff) * h) / (1.5 * h) + 1);
|
||||
|
||||
let snapX = c * w - (1 - xOff) * w;
|
||||
let snapY = r * h * 1.5 - (1.5 - yOff) * h;
|
||||
|
||||
return {x: snapX, y: snapY};
|
||||
}
|
||||
|
||||
function findSnapPointCols(x, y, h, w, alt) {
|
||||
let yOffset = 0.0;
|
||||
if (canvas.grid.grid.even) {
|
||||
yOffset = -0.5;
|
||||
}
|
||||
|
||||
let xOffset1 = 0.25;
|
||||
let xOffset2 = 1.0;
|
||||
if (alt) {
|
||||
xOffset1 = 0.75;
|
||||
xOffset2 = 0.0;
|
||||
}
|
||||
|
||||
let row1 = calculateSnapPointsCols(x, y, h, w, xOffset1, 0.5 + yOffset);
|
||||
let row2 = calculateSnapPointsCols(x, y, h, w, xOffset2, 1.0 + yOffset);
|
||||
|
||||
let dist1 = Math.pow(row1.x - x, 2) + Math.pow(row1.y - y, 2);
|
||||
let dist2 = Math.pow(row2.x - x, 2) + Math.pow(row2.y - y, 2);
|
||||
|
||||
if (dist1 < dist2) {
|
||||
return row1;
|
||||
} else {
|
||||
return row2;
|
||||
}
|
||||
}
|
||||
|
||||
function calculateSnapPointsCols(x, y, h, w, xOff, yOff) {
|
||||
let c = Math.floor((x + (0.75 - xOff) * w) / (1.5 * w) + 1);
|
||||
let r = Math.floor((y + (0.5 - yOff) * h) / h + 1);
|
||||
|
||||
let snapX = c * w * 1.5 - (1.5 - xOff) * w;
|
||||
let snapY = r * h - (1 - yOff) * h;
|
||||
|
||||
return {x: snapX, y: snapY};
|
||||
}
|
||||
+64
-24
@@ -1,25 +1,35 @@
|
||||
import {settingsKey} from "./settings.js";
|
||||
import {getMeasurePosition, setSnapParameterOnOptions} from "./util.js";
|
||||
|
||||
// Indicates whether the user is currently pressing the button to disable snapping
|
||||
export let disableSnap = false;
|
||||
|
||||
// Indicates whether the user is currently pressing the button to disable the movement animation
|
||||
export let moveWithoutAnimation = false;
|
||||
|
||||
// Indicates whether the user is pressing the pathfinding toggle button
|
||||
export let togglePathfinding = false;
|
||||
|
||||
export function registerKeybindings() {
|
||||
game.keybindings.register(settingsKey, "cancelDrag", {
|
||||
name: "drag-ruler.keybindings.cancelDrag",
|
||||
onDown: cancelDrag,
|
||||
uneditable: [{
|
||||
key: "Escape",
|
||||
}],
|
||||
uneditable: [
|
||||
{
|
||||
key: "Escape",
|
||||
},
|
||||
],
|
||||
precedence: -1,
|
||||
});
|
||||
|
||||
game.keybindings.register(settingsKey, "createWaypoint", {
|
||||
name: "drag-ruler.keybindings.createWaypoint",
|
||||
onDown: handleCreateWaypoint,
|
||||
editable: [{
|
||||
key: "Space"
|
||||
}],
|
||||
editable: [
|
||||
{
|
||||
key: "Space",
|
||||
},
|
||||
],
|
||||
precedence: -1,
|
||||
});
|
||||
|
||||
@@ -34,9 +44,11 @@ export function registerKeybindings() {
|
||||
hint: "drag-ruler.keybindings.disableSnap.hint",
|
||||
onDown: handleDisableSnap,
|
||||
onUp: handleDisableSnap,
|
||||
editable: [{
|
||||
key: "ShiftLeft",
|
||||
}],
|
||||
editable: [
|
||||
{
|
||||
key: "ShiftLeft",
|
||||
},
|
||||
],
|
||||
precedence: -1,
|
||||
});
|
||||
|
||||
@@ -45,17 +57,29 @@ export function registerKeybindings() {
|
||||
hint: "drag-ruler.keybindings.moveWithoutAnimation.hint",
|
||||
onDown: handleMoveWithoutAnimation,
|
||||
onUp: handleMoveWithoutAnimation,
|
||||
editable: [{
|
||||
key: "AltLeft",
|
||||
}],
|
||||
editable: [
|
||||
{
|
||||
key: "AltLeft",
|
||||
},
|
||||
],
|
||||
precedence: -1,
|
||||
});
|
||||
|
||||
if (game.modules.get("routinglib")?.active) {
|
||||
game.keybindings.register(settingsKey, "togglePathfinding", {
|
||||
name: "drag-ruler.keybindings.togglePathfinding.name",
|
||||
hint: "drag-ruler.keybindings.togglePathfinding.hint",
|
||||
onDown: handleTogglePathfinding,
|
||||
onUp: handleTogglePathfinding,
|
||||
precedence: -1,
|
||||
restricted: !game.settings.get(settingsKey, "allowPathfinding"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteWaypoint() {
|
||||
const ruler = canvas.controls.ruler;
|
||||
if (!ruler?.draggedEntity)
|
||||
return false;
|
||||
if (!ruler?.draggedEntity) return false;
|
||||
ruler.dragRulerDeleteWaypoint();
|
||||
return true;
|
||||
}
|
||||
@@ -64,16 +88,14 @@ function handleCreateWaypoint() {
|
||||
const ruler = canvas.controls.ruler;
|
||||
// .draggedEntity is used here because .isDragRuler only returns true once the ruler started measuring
|
||||
// Ruler can end up being undefined here if no canvas is active
|
||||
if (!ruler?.draggedEntity)
|
||||
return false;
|
||||
if (!ruler?.draggedEntity) return false;
|
||||
|
||||
let options = {};
|
||||
setSnapParameterOnOptions(ruler, options);
|
||||
|
||||
if (ruler._state === Ruler.STATES.INACTIVE) {
|
||||
ruler.dragRulerStart(options);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
ruler.dragRulerAddWaypoint(getMeasurePosition(), options);
|
||||
}
|
||||
return true;
|
||||
@@ -81,8 +103,7 @@ function handleCreateWaypoint() {
|
||||
|
||||
function cancelDrag() {
|
||||
const ruler = canvas.controls.ruler;
|
||||
if (!ruler?.draggedEntity)
|
||||
return false;
|
||||
if (!ruler?.draggedEntity) return false;
|
||||
ruler.dragRulerAbortDrag();
|
||||
return true;
|
||||
}
|
||||
@@ -91,15 +112,34 @@ function handleDisableSnap(event) {
|
||||
disableSnap = !event.up;
|
||||
|
||||
const ruler = canvas.controls.ruler;
|
||||
if (!ruler?.isDragRuler)
|
||||
return false;
|
||||
if (ruler._state !== Ruler.STATES.MEASURING)
|
||||
return false;
|
||||
if (!ruler?.isDragRuler) return false;
|
||||
if (ruler._state !== Ruler.STATES.MEASURING) return false;
|
||||
|
||||
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
|
||||
ruler.dragRulerSendState();
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleMoveWithoutAnimation(event) {
|
||||
moveWithoutAnimation = !event.up;
|
||||
|
||||
const ruler = canvas.controls.ruler;
|
||||
if (!ruler?.isDragRuler) return false;
|
||||
if (ruler._state !== Ruler.STATES.MEASURING) return false;
|
||||
|
||||
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
|
||||
ruler.dragRulerSendState();
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleTogglePathfinding(event) {
|
||||
togglePathfinding = !event.up;
|
||||
|
||||
const ruler = canvas.controls.ruler;
|
||||
if (!ruler?.isDragRuler) return false;
|
||||
if (ruler._state !== Ruler.STATES.MEASURING) return false;
|
||||
|
||||
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
|
||||
ruler.dragRulerSendState();
|
||||
return false;
|
||||
}
|
||||
|
||||
+48
-31
@@ -1,73 +1,90 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2021 fvtt-lib-wrapper Rui Pinheiro
|
||||
|
||||
|
||||
'use strict';
|
||||
"use strict";
|
||||
|
||||
// A shim for the libWrapper library
|
||||
export let libWrapper = undefined;
|
||||
|
||||
export const VERSIONS = [1,11,0];
|
||||
export const TGT_SPLIT_RE = new RegExp("([^.[]+|\\[('([^'\\\\]|\\\\.)+?'|\"([^\"\\\\]|\\\\.)+?\")\\])", 'g');
|
||||
export const TGT_CLEANUP_RE = new RegExp("(^\\['|'\\]$|^\\[\"|\"\\]$)", 'g');
|
||||
export const VERSIONS = [1, 11, 0];
|
||||
export const TGT_SPLIT_RE = new RegExp(
|
||||
"([^.[]+|\\[('([^'\\\\]|\\\\.)+?'|\"([^\"\\\\]|\\\\.)+?\")\\])",
|
||||
"g",
|
||||
);
|
||||
export const TGT_CLEANUP_RE = new RegExp("(^\\['|'\\]$|^\\[\"|\"\\]$)", "g");
|
||||
|
||||
// Main shim code
|
||||
Hooks.once('init', () => {
|
||||
Hooks.once("init", () => {
|
||||
// Check if the real module is already loaded - if so, use it
|
||||
if(globalThis.libWrapper && !(globalThis.libWrapper.is_fallback ?? true)) {
|
||||
if (globalThis.libWrapper && !(globalThis.libWrapper.is_fallback ?? true)) {
|
||||
libWrapper = globalThis.libWrapper;
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback implementation
|
||||
libWrapper = class {
|
||||
static get is_fallback() { return true };
|
||||
static get is_fallback() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static get WRAPPER() { return 'WRAPPER' };
|
||||
static get MIXED() { return 'MIXED' };
|
||||
static get OVERRIDE() { return 'OVERRIDE' };
|
||||
static get WRAPPER() {
|
||||
return "WRAPPER";
|
||||
}
|
||||
static get MIXED() {
|
||||
return "MIXED";
|
||||
}
|
||||
static get OVERRIDE() {
|
||||
return "OVERRIDE";
|
||||
}
|
||||
|
||||
static register(package_id, target, fn, type="MIXED", {chain=undefined}={}) {
|
||||
const is_setter = target.endsWith('#set');
|
||||
static register(package_id, target, fn, type = "MIXED", {chain = undefined} = {}) {
|
||||
const is_setter = target.endsWith("#set");
|
||||
target = !is_setter ? target : target.slice(0, -4);
|
||||
const split = target.match(TGT_SPLIT_RE).map((x)=>x.replace(/\\(.)/g, '$1').replace(TGT_CLEANUP_RE,''));
|
||||
const root_nm = split.splice(0,1)[0];
|
||||
const split = target
|
||||
.match(TGT_SPLIT_RE)
|
||||
.map(x => x.replace(/\\(.)/g, "$1").replace(TGT_CLEANUP_RE, ""));
|
||||
const root_nm = split.splice(0, 1)[0];
|
||||
|
||||
let obj, fn_name;
|
||||
if(split.length == 0) {
|
||||
if (split.length == 0) {
|
||||
obj = globalThis;
|
||||
fn_name = root_nm;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const _eval = eval;
|
||||
fn_name = split.pop();
|
||||
obj = split.reduce((x,y)=>x[y], globalThis[root_nm] ?? _eval(root_nm));
|
||||
obj = split.reduce((x, y) => x[y], globalThis[root_nm] ?? _eval(root_nm));
|
||||
}
|
||||
|
||||
let iObj = obj;
|
||||
let descriptor = null;
|
||||
while(iObj) {
|
||||
while (iObj) {
|
||||
descriptor = Object.getOwnPropertyDescriptor(iObj, fn_name);
|
||||
if(descriptor) break;
|
||||
if (descriptor) break;
|
||||
iObj = Object.getPrototypeOf(iObj);
|
||||
}
|
||||
if(!descriptor || descriptor?.configurable === false) throw `libWrapper Shim: '${target}' does not exist, could not be found, or has a non-configurable descriptor.`;
|
||||
if (!descriptor || descriptor?.configurable === false)
|
||||
throw `libWrapper Shim: '${target}' does not exist, could not be found, or has a non-configurable descriptor.`;
|
||||
|
||||
let original = null;
|
||||
const wrapper = (chain ?? (type.toUpperCase?.() != 'OVERRIDE' && type != 3)) ? function() { return fn.call(this, original.bind(this), ...arguments); } : function() { return fn.apply(this, arguments); };
|
||||
const wrapper =
|
||||
chain ?? (type.toUpperCase?.() != "OVERRIDE" && type != 3)
|
||||
? function () {
|
||||
return fn.call(this, original.bind(this), ...arguments);
|
||||
}
|
||||
: function () {
|
||||
return fn.apply(this, arguments);
|
||||
};
|
||||
|
||||
if(!is_setter) {
|
||||
if(descriptor.value) {
|
||||
if (!is_setter) {
|
||||
if (descriptor.value) {
|
||||
original = descriptor.value;
|
||||
descriptor.value = wrapper;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
original = descriptor.get;
|
||||
descriptor.get = wrapper;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(!descriptor.set) throw `libWrapper Shim: '${target}' does not have a setter`;
|
||||
} else {
|
||||
if (!descriptor.set) throw `libWrapper Shim: '${target}' does not have a setter`;
|
||||
original = descriptor.set;
|
||||
descriptor.set = wrapper;
|
||||
}
|
||||
@@ -75,5 +92,5 @@ Hooks.once('init', () => {
|
||||
descriptor.configurable = true;
|
||||
Object.defineProperty(obj, fn_name, descriptor);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
+114
-70
@@ -1,25 +1,40 @@
|
||||
"use strict"
|
||||
"use strict";
|
||||
|
||||
import {getColorForDistanceAndToken, getMovedDistanceFromToken, getRangesFromSpeedProvider, initApi, registerModule, registerSystem} from "./api.js";
|
||||
import {
|
||||
getColorForDistanceAndToken,
|
||||
getMovedDistanceFromToken,
|
||||
getRangesFromSpeedProvider,
|
||||
initApi,
|
||||
registerModule,
|
||||
registerSystem,
|
||||
} from "./api.js";
|
||||
import {checkDependencies, getHexSizeSupportTokenGridCenter} from "./compatibility.js";
|
||||
import {moveEntities, onMouseMove} from "./foundry_imports.js"
|
||||
import {moveEntities, onMouseMove} from "./foundry_imports.js";
|
||||
import {disableSnap, registerKeybindings} from "./keybindings.js";
|
||||
import {libWrapper} from "./libwrapper_shim.js";
|
||||
import {performMigrations} from "./migration.js"
|
||||
import {performMigrations} from "./migration.js";
|
||||
import {removeLastHistoryEntryIfAt, resetMovementHistory} from "./movement_tracking.js";
|
||||
import {extendRuler} from "./ruler.js";
|
||||
import {registerSettings, RightClickAction, settingsKey} from "./settings.js"
|
||||
import {registerSettings, RightClickAction, settingsKey} from "./settings.js";
|
||||
import {recalculate} from "./socket.js";
|
||||
import {SpeedProvider} from "./speed_provider.js"
|
||||
import {SpeedProvider} from "./speed_provider.js";
|
||||
import {setSnapParameterOnOptions} from "./util.js";
|
||||
|
||||
CONFIG.debug.dragRuler = false;
|
||||
export let debugGraphics = undefined;
|
||||
|
||||
Hooks.once("init", () => {
|
||||
registerSettings()
|
||||
registerKeybindings()
|
||||
initApi()
|
||||
registerSettings();
|
||||
registerKeybindings();
|
||||
initApi();
|
||||
hookDragHandlers(Token);
|
||||
hookDragHandlers(MeasuredTemplate);
|
||||
libWrapper.register("drag-ruler", "TokenLayer.prototype.undoHistory", tokenLayerUndoHistory, "WRAPPER");
|
||||
libWrapper.register(
|
||||
"drag-ruler",
|
||||
"TokenLayer.prototype.undoHistory",
|
||||
tokenLayerUndoHistory,
|
||||
"WRAPPER",
|
||||
);
|
||||
|
||||
extendRuler();
|
||||
|
||||
@@ -30,14 +45,15 @@ Hooks.once("init", () => {
|
||||
registerSystem,
|
||||
recalculate,
|
||||
resetMovementHistory,
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
Hooks.once("ready", () => {
|
||||
performMigrations()
|
||||
performMigrations();
|
||||
checkDependencies();
|
||||
Hooks.callAll("dragRuler.ready", SpeedProvider)
|
||||
})
|
||||
Hooks.callAll("dragRuler.ready", SpeedProvider);
|
||||
if (CONFIG.debug.dragRuler) debugGraphics = canvas.controls.addChild(new PIXI.Container());
|
||||
});
|
||||
|
||||
Hooks.on("canvasReady", () => {
|
||||
canvas.controls.rulers.children.forEach(ruler => {
|
||||
@@ -45,37 +61,61 @@ Hooks.on("canvasReady", () => {
|
||||
Object.defineProperty(ruler, "isDragRuler", {
|
||||
get: function isDragRuler() {
|
||||
return Boolean(this.draggedEntity) && this._state !== Ruler.STATES.INACTIVE;
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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.viewed, li.data('combatant-id')),
|
||||
callback: li => resetMovementHistory(ui.combat.viewed, li.data("combatant-id")),
|
||||
};
|
||||
menu.splice(1, 0, entry);
|
||||
});
|
||||
|
||||
function forwardIfUnahndled(newFn) {
|
||||
return function(oldFn, ...args) {
|
||||
return function (oldFn, ...args) {
|
||||
const eventHandled = newFn(...args);
|
||||
if (!eventHandled)
|
||||
oldFn(...args);
|
||||
if (!eventHandled) oldFn(...args);
|
||||
};
|
||||
}
|
||||
|
||||
function hookDragHandlers(entityType) {
|
||||
const entityName = entityType.name
|
||||
libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftStart`, onEntityLeftDragStart, "WRAPPER");
|
||||
const entityName = entityType.name;
|
||||
libWrapper.register(
|
||||
"drag-ruler",
|
||||
`${entityName}.prototype._onDragLeftStart`,
|
||||
onEntityLeftDragStart,
|
||||
"WRAPPER",
|
||||
);
|
||||
if (entityType === Token)
|
||||
libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftMove`, onEntityLeftDragMoveSnap, "WRAPPER");
|
||||
libWrapper.register(
|
||||
"drag-ruler",
|
||||
`${entityName}.prototype._onDragLeftMove`,
|
||||
onEntityLeftDragMoveSnap,
|
||||
"WRAPPER",
|
||||
);
|
||||
else
|
||||
libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftMove`, onEntityLeftDragMove, "WRAPPER");
|
||||
libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftDrop`, forwardIfUnahndled(onEntityDragLeftDrop), "MIXED");
|
||||
libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftCancel`, forwardIfUnahndled(onEntityDragLeftCancel), "MIXED");
|
||||
libWrapper.register(
|
||||
"drag-ruler",
|
||||
`${entityName}.prototype._onDragLeftMove`,
|
||||
onEntityLeftDragMove,
|
||||
"WRAPPER",
|
||||
);
|
||||
libWrapper.register(
|
||||
"drag-ruler",
|
||||
`${entityName}.prototype._onDragLeftDrop`,
|
||||
forwardIfUnahndled(onEntityDragLeftDrop),
|
||||
"MIXED",
|
||||
);
|
||||
libWrapper.register(
|
||||
"drag-ruler",
|
||||
`${entityName}.prototype._onDragLeftCancel`,
|
||||
forwardIfUnahndled(onEntityDragLeftCancel),
|
||||
"MIXED",
|
||||
);
|
||||
}
|
||||
|
||||
async function tokenLayerUndoHistory(wrapped) {
|
||||
@@ -93,14 +133,21 @@ async function tokenLayerUndoHistory(wrapped) {
|
||||
function onEntityLeftDragStart(wrapped, event) {
|
||||
wrapped(event);
|
||||
const isToken = this instanceof Token;
|
||||
const ruler = canvas.controls.ruler
|
||||
const ruler = canvas.controls.ruler;
|
||||
ruler.draggedEntity = this;
|
||||
let entityCenter;
|
||||
if (isToken && canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(this))
|
||||
if (
|
||||
isToken &&
|
||||
canvas.grid.isHex &&
|
||||
game.modules.get("hex-size-support")?.active &&
|
||||
CONFIG.hexSizeSupport.getAltSnappingFlag(this)
|
||||
)
|
||||
entityCenter = getHexSizeSupportTokenGridCenter(this);
|
||||
else
|
||||
entityCenter = this.center;
|
||||
ruler.rulerOffset = {x: entityCenter.x - event.data.origin.x, y: entityCenter.y - event.data.origin.y};
|
||||
else entityCenter = this.center;
|
||||
ruler.rulerOffset = {
|
||||
x: entityCenter.x - event.data.origin.x,
|
||||
y: entityCenter.y - event.data.origin.y,
|
||||
};
|
||||
if (game.settings.get(settingsKey, "autoStartMeasurement")) {
|
||||
let options = {};
|
||||
setSnapParameterOnOptions(ruler, options);
|
||||
@@ -115,48 +162,42 @@ function onEntityLeftDragMoveSnap(wrapped, event) {
|
||||
|
||||
function onEntityLeftDragMove(wrapped, event) {
|
||||
wrapped(event);
|
||||
const ruler = canvas.controls.ruler
|
||||
if (ruler.isDragRuler)
|
||||
onMouseMove.call(ruler, event)
|
||||
const ruler = canvas.controls.ruler;
|
||||
if (ruler.isDragRuler) onMouseMove.call(ruler, event);
|
||||
}
|
||||
|
||||
function onEntityDragLeftDrop(event) {
|
||||
const ruler = canvas.controls.ruler
|
||||
const ruler = canvas.controls.ruler;
|
||||
if (!ruler.isDragRuler) {
|
||||
ruler.draggedEntity = undefined;
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
// When we're dragging a measured template no token will ever be selected,
|
||||
// resulting in only the dragged template to be moved as would be expected
|
||||
const selectedTokens = canvas.tokens.controlled
|
||||
const selectedTokens = canvas.tokens.controlled;
|
||||
// This can happen if the user presses ESC during drag (maybe there are other ways too)
|
||||
if (selectedTokens.length === 0)
|
||||
selectedTokens.push(ruler.draggedEntity);
|
||||
ruler._state = Ruler.STATES.MOVING
|
||||
if (ruler.draggedEntity instanceof Token && game.settings.get("core", "tokenDragPreview")) {
|
||||
event.data.clones.forEach(c => c._original.updateSource({skipUpdateFog: true, defer: true}));
|
||||
}
|
||||
if (selectedTokens.length === 0) selectedTokens.push(ruler.draggedEntity);
|
||||
// This can happen if the ruler is being dragged so rapidly that the drag move handler hasn't been called before dropping
|
||||
if (ruler._state === Ruler.STATES.STARTING) onMouseMove.call(ruler, event);
|
||||
ruler._state = Ruler.STATES.MOVING;
|
||||
moveEntities.call(ruler, ruler.draggedEntity, selectedTokens);
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
|
||||
function onEntityDragLeftCancel(event) {
|
||||
// This function is invoked by right clicking
|
||||
const ruler = canvas.controls.ruler
|
||||
if (!ruler.draggedEntity || ruler._state === Ruler.STATES.MOVING)
|
||||
return false
|
||||
const ruler = canvas.controls.ruler;
|
||||
if (!ruler.draggedEntity || ruler._state === Ruler.STATES.MOVING) return false;
|
||||
|
||||
const rightClickAction = game.settings.get(settingsKey, "rightClickAction");
|
||||
let options = {};
|
||||
setSnapParameterOnOptions(ruler, options);
|
||||
|
||||
if (ruler._state === Ruler.STATES.INACTIVE) {
|
||||
if (rightClickAction !== RightClickAction.CREATE_WAYPOINT)
|
||||
return false;
|
||||
if (rightClickAction !== RightClickAction.CREATE_WAYPOINT) return false;
|
||||
ruler.dragRulerStart(options);
|
||||
event.preventDefault();
|
||||
}
|
||||
else if (ruler._state === Ruler.STATES.MEASURING) {
|
||||
} else if (ruler._state === Ruler.STATES.MEASURING) {
|
||||
switch (rightClickAction) {
|
||||
case RightClickAction.CREATE_WAYPOINT:
|
||||
event.preventDefault();
|
||||
@@ -175,14 +216,10 @@ function onEntityDragLeftCancel(event) {
|
||||
|
||||
function applyGridlessSnapping(event) {
|
||||
const ruler = canvas.controls.ruler;
|
||||
if (!game.settings.get(settingsKey, "useGridlessRaster"))
|
||||
return;
|
||||
if (!ruler.isDragRuler)
|
||||
return;
|
||||
if (disableSnap)
|
||||
return;
|
||||
if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS)
|
||||
return;
|
||||
if (!game.settings.get(settingsKey, "useGridlessRaster")) return;
|
||||
if (!ruler.isDragRuler) return;
|
||||
if (disableSnap) return;
|
||||
if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS) return;
|
||||
|
||||
const rasterWidth = 35 / canvas.stage.scale.x;
|
||||
const tokenX = event.data.destination.x;
|
||||
@@ -192,7 +229,11 @@ function applyGridlessSnapping(event) {
|
||||
|
||||
const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active;
|
||||
if (terrainRulerAvailable) {
|
||||
const segments = Ruler.dragRulerGetRaysFromWaypoints(ruler.waypoints, destination).map(ray => {return {ray}});
|
||||
const segments = ruler.constructor
|
||||
.dragRulerGetRaysFromWaypoints(ruler.waypoints, destination)
|
||||
.map(ray => {
|
||||
return {ray};
|
||||
});
|
||||
const pinpointDistances = new Map();
|
||||
for (const range of ranges) {
|
||||
pinpointDistances.set(range.range, null);
|
||||
@@ -200,7 +241,7 @@ function applyGridlessSnapping(event) {
|
||||
terrainRuler.measureDistances(segments, {pinpointDistances});
|
||||
const targetDistance = Array.from(pinpointDistances.entries())
|
||||
.filter(([_key, val]) => val)
|
||||
.reduce((value, current) => value[0] > current[0] ? value : current, [0, null]);
|
||||
.reduce((value, current) => (value[0] > current[0] ? value : current), [0, null]);
|
||||
const rasterLocation = targetDistance[1];
|
||||
if (rasterLocation) {
|
||||
const deltaX = destination.x - rasterLocation.x;
|
||||
@@ -211,12 +252,15 @@ function applyGridlessSnapping(event) {
|
||||
event.data.destination.y = rasterLocation.y - ruler.rulerOffset.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
let waypointDistance = 0;
|
||||
let origin = event.data.origin;
|
||||
if (ruler.waypoints.length > 1) {
|
||||
const segments = Ruler.dragRulerGetRaysFromWaypoints(ruler.waypoints, destination).map(ray => {return {ray}});
|
||||
const segments = ruler.constructor
|
||||
.dragRulerGetRaysFromWaypoints(ruler.waypoints, destination)
|
||||
.map(ray => {
|
||||
return {ray};
|
||||
});
|
||||
origin = segments.pop().ray.A;
|
||||
waypointDistance = canvas.grid.measureDistances(segments).reduce((a, b) => a + b);
|
||||
origin = {x: origin.x - ruler.rulerOffset.x, y: origin.y - ruler.rulerOffset.y};
|
||||
@@ -229,13 +273,13 @@ function applyGridlessSnapping(event) {
|
||||
let targetDistance = ranges
|
||||
.map(range => range.range)
|
||||
.map(range => range - waypointDistance)
|
||||
.map(range => range * canvas.dimensions.size / canvas.dimensions.distance)
|
||||
.map(range => (range * canvas.dimensions.size) / canvas.dimensions.distance)
|
||||
.filter(range => range < distance)
|
||||
.reduce((a, b) => Math.max(a, b), 0);
|
||||
if (targetDistance) {
|
||||
if (distance < targetDistance + rasterWidth) {
|
||||
event.data.destination.x = origin.x + deltaX * targetDistance / distance;
|
||||
event.data.destination.y = origin.y + deltaY * targetDistance / distance;
|
||||
event.data.destination.x = origin.x + (deltaX * targetDistance) / distance;
|
||||
event.data.destination.y = origin.y + (deltaY * targetDistance) / distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-12
@@ -1,26 +1,24 @@
|
||||
import {RightClickAction, settingsKey} from "./settings.js"
|
||||
import {RightClickAction, settingsKey} from "./settings.js";
|
||||
|
||||
const currentDataVersion = "1.10.0"
|
||||
const currentDataVersion = "1.10.0";
|
||||
|
||||
export async function performMigrations() {
|
||||
if (game.user.isGM)
|
||||
await performWorldMigraionts()
|
||||
await performClientMigrations()
|
||||
if (game.user.isGM) await performWorldMigraionts();
|
||||
await performClientMigrations();
|
||||
}
|
||||
|
||||
async function performWorldMigraionts() {
|
||||
let dataVersion = game.settings.get(settingsKey, "dataVersion");
|
||||
|
||||
if (dataVersion === currentDataVersion)
|
||||
return;
|
||||
if (dataVersion === currentDataVersion) return;
|
||||
|
||||
if (dataVersion === "fresh install") {
|
||||
game.settings.set(settingsKey, "dataVersion", currentDataVersion)
|
||||
return
|
||||
game.settings.set(settingsKey, "dataVersion", currentDataVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataVersion === "1.3.0") {
|
||||
dataVersion = "1.10.0"
|
||||
dataVersion = "1.10.0";
|
||||
}
|
||||
|
||||
game.settings.set(settingsKey, "dataVersion", dataVersion);
|
||||
@@ -31,13 +29,15 @@ async function performClientMigrations() {
|
||||
|
||||
if (dataVersion === "fresh install") {
|
||||
// Start of migration from unnamed version (< 1.10.0). TODO Remove in a future version
|
||||
const swapSpacebarRightClick = game.settings.storage.get("client").getItem(`${settingsKey}.swapSpacebarRightClick`);
|
||||
const swapSpacebarRightClick = game.settings.storage
|
||||
.get("client")
|
||||
.getItem(`${settingsKey}.swapSpacebarRightClick`);
|
||||
if (swapSpacebarRightClick) {
|
||||
game.settings.set(settingsKey, "rightClickAction", RightClickAction.CREATE_WAYPOINT);
|
||||
await game.keybindings.set(settingsKey, "createWaypoint", []);
|
||||
await game.keybindings.set(settingsKey, "deleteWaypoint", [{key: "Space"}]);
|
||||
}
|
||||
// End of migration from unnamed version
|
||||
game.settings.set(settingsKey, "clientDataVersion", currentDataVersion)
|
||||
game.settings.set(settingsKey, "clientDataVersion", currentDataVersion);
|
||||
}
|
||||
}
|
||||
|
||||
+22
-26
@@ -9,38 +9,35 @@ function initTrackingFlag(combatant) {
|
||||
if (isNaN(dragRulerFlag.trackedRound)) {
|
||||
mergeObject(dragRulerFlag, initialFlag);
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
combatant.data.flags.dragRuler = initialFlag;
|
||||
}
|
||||
}
|
||||
|
||||
function getInitializedCombatant(token, combat) {
|
||||
const combatant = combat.getCombatantByToken(token.id);
|
||||
if (!combatant)
|
||||
return undefined;
|
||||
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 (!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);
|
||||
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;
|
||||
if (!combatant) return;
|
||||
|
||||
// Check if we have entered a new round. If so, remove the currently stored path
|
||||
if (combat.data.round > combatant.data.flags.dragRuler.trackedRound) {
|
||||
@@ -56,7 +53,10 @@ function calculateUpdate(combat, token, rays) {
|
||||
// Ignore rays that have the same start and end coordinates
|
||||
if (ray.A.x !== ray.B.x || ray.A.y !== ray.B.y) {
|
||||
if (terrainRulerAvailable) {
|
||||
measureDistances([{ray}], token, getTokenShape(token), {terrainRulerInitialState: waypoints[waypoints.length - 1]?.dragRulerFinalState, enableTerrainRuler: terrainRulerAvailable});
|
||||
measureDistances([{ray}], token, getTokenShape(token), {
|
||||
terrainRulerInitialState: waypoints[waypoints.length - 1]?.dragRulerFinalState,
|
||||
enableTerrainRuler: terrainRulerAvailable,
|
||||
});
|
||||
ray.A.dragRulerVisitedSpaces = ray.terrainRulerVisitedSpaces;
|
||||
ray.A.dragRulerFinalState = ray.terrainRulerFinalState;
|
||||
}
|
||||
@@ -68,23 +68,18 @@ function calculateUpdate(combat, token, rays) {
|
||||
|
||||
export function getMovementHistory(token) {
|
||||
const combat = game.combat;
|
||||
if (!combat)
|
||||
return [];
|
||||
if (!combat) return [];
|
||||
const combatant = combat.getCombatantByToken(token.id);
|
||||
if (!combatant)
|
||||
return [];
|
||||
const dragRulerFlags = combatant.data.flags.dragRuler;
|
||||
if (!dragRulerFlags)
|
||||
return [];
|
||||
if (combat.data.round > dragRulerFlags.trackedRound)
|
||||
return [];
|
||||
if (!combatant) return [];
|
||||
const dragRulerFlags = combatant.flags.dragRuler;
|
||||
if (!dragRulerFlags) return [];
|
||||
if (combat.round > dragRulerFlags.trackedRound) return [];
|
||||
return dragRulerFlags.passedWaypoints ?? [];
|
||||
}
|
||||
|
||||
export async function removeLastHistoryEntryIfAt(token, x, y) {
|
||||
const history = getMovementHistory(token);
|
||||
if (history.length === 0)
|
||||
return;
|
||||
if (history.length === 0) return;
|
||||
const entry = history[history.length - 1];
|
||||
if (!isClose(x + token.w / 2, entry.x, 0.1) || !isClose(y + token.h / 2, entry.y, 0.1)) {
|
||||
return;
|
||||
@@ -92,14 +87,15 @@ export async function removeLastHistoryEntryIfAt(token, x, y) {
|
||||
history.pop();
|
||||
const combat = game.combat;
|
||||
const combatant = combat.getCombatantByToken(token.id);
|
||||
await updateCombatantDragRulerFlags(combat, [{_id: combatant.id, dragRulerFlags: combatant.data.flags.dragRuler}]);
|
||||
await updateCombatantDragRulerFlags(combat, [
|
||||
{_id: combatant.id, dragRulerFlags: combatant.data.flags.dragRuler},
|
||||
]);
|
||||
}
|
||||
|
||||
export async function resetMovementHistory(combat, combatantId) {
|
||||
const combatant = combat.combatants.get(combatantId);
|
||||
const dragRulerFlags = combatant.data.flags.dragRuler;
|
||||
if (!dragRulerFlags)
|
||||
return;
|
||||
if (!dragRulerFlags) return;
|
||||
dragRulerFlags.passedWaypoints = null;
|
||||
dragRulerFlags.trackedRound = null;
|
||||
dragRulerFlags.rulerState = null;
|
||||
|
||||
+374
-50
@@ -1,14 +1,30 @@
|
||||
import {currentSpeedProvider, getColorForDistanceAndToken, getRangesFromSpeedProvider} from "./api.js";
|
||||
import {getHexSizeSupportTokenGridCenter} from "./compatibility.js";
|
||||
import {cancelScheduledMeasurement, measure} from "./foundry_imports.js"
|
||||
import {
|
||||
currentSpeedProvider,
|
||||
getColorForDistanceAndToken,
|
||||
getRangesFromSpeedProvider,
|
||||
} from "./api.js";
|
||||
import {
|
||||
getHexSizeSupportTokenGridCenter,
|
||||
highlightMeasurementTerrainRuler,
|
||||
measureDistances,
|
||||
} from "./compatibility.js";
|
||||
import {getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
|
||||
import {cancelScheduledMeasurement, highlightMeasurementNative} from "./foundry_imports.js";
|
||||
import {disableSnap} from "./keybindings.js";
|
||||
import {getMovementHistory} from "./movement_tracking.js";
|
||||
import {settingsKey} from "./settings.js";
|
||||
import {getSnapPointForEntity} from "./util.js";
|
||||
import {
|
||||
applyTokenSizeOffset,
|
||||
getSnapPointForEntity,
|
||||
getSnapPointForTokenObj,
|
||||
getTokenShape,
|
||||
isPathfindingEnabled,
|
||||
} from "./util.js";
|
||||
|
||||
export function extendRuler() {
|
||||
class DragRulerRuler extends Ruler {
|
||||
class DragRulerRuler extends CONFIG.Canvas.rulerClass {
|
||||
// Functions below are overridden versions of functions in Ruler
|
||||
constructor(user, {color=null}={}) {
|
||||
constructor(user, {color = null} = {}) {
|
||||
super(user, {color});
|
||||
this.previousWaypoints = [];
|
||||
this.previousLabels = this.addChild(new PIXI.Container());
|
||||
@@ -24,61 +40,325 @@ export function extendRuler() {
|
||||
|
||||
async moveToken(event) {
|
||||
// Disable moveToken if Drag Ruler is active
|
||||
if (!this.isDragRuler)
|
||||
return await super.moveToken(event);
|
||||
if (!this.isDragRuler) return await super.moveToken(event);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const json = super.toJSON();
|
||||
if (this.draggedEntity) {
|
||||
const isToken = this.draggedEntity instanceof Token;
|
||||
json["draggedEntityIsToken"] = isToken;
|
||||
json["draggedEntity"] = this.draggedEntity.id;
|
||||
json.draggedEntityIsToken = isToken;
|
||||
json.draggedEntity = this.draggedEntity.id;
|
||||
json.waypoints = json.waypoints.map(old => {
|
||||
let w = duplicate(old);
|
||||
w.isPathfinding = undefined;
|
||||
return w;
|
||||
});
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
update(data) {
|
||||
// Don't show a GMs drag ruler to non GM players
|
||||
if (data.draggedEntity && this.user.isGM && !game.user.isGM && !game.settings.get(settingsKey, "showGMRulerToPlayers"))
|
||||
if (
|
||||
data.draggedEntity &&
|
||||
this.user.isGM &&
|
||||
!game.user.isGM &&
|
||||
!game.settings.get(settingsKey, "showGMRulerToPlayers")
|
||||
)
|
||||
return;
|
||||
|
||||
if (data.draggedEntity) {
|
||||
if (data.draggedEntityIsToken)
|
||||
this.draggedEntity = canvas.tokens.get(data.draggedEntity);
|
||||
else
|
||||
this.draggedEntity = canvas.templates.get(data.draggedEntity);
|
||||
if (data.draggedEntityIsToken) this.draggedEntity = canvas.tokens.get(data.draggedEntity);
|
||||
else this.draggedEntity = canvas.templates.get(data.draggedEntity);
|
||||
} else {
|
||||
this.draggedEntity = undefined;
|
||||
}
|
||||
|
||||
super.update(data);
|
||||
}
|
||||
|
||||
measure(destination, options={}) {
|
||||
if (this.isDragRuler) {
|
||||
return measure.call(this, destination, options);
|
||||
}
|
||||
else {
|
||||
measure(destination, options = {}) {
|
||||
if (!this.isDragRuler) {
|
||||
return super.measure(destination, options);
|
||||
}
|
||||
if (options.gridSpaces === undefined) {
|
||||
options.gridSpaces = canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS;
|
||||
}
|
||||
this.dragRulerGridSpaces = options.gridSpaces;
|
||||
const isToken = this.draggedEntity instanceof Token;
|
||||
if (isToken && !this.draggedEntity.isVisible) {
|
||||
return [];
|
||||
}
|
||||
if (canvas.grid.diagonalRule === "EUCL") {
|
||||
options.gridSpaces = false;
|
||||
options.ignoreGrid = true;
|
||||
}
|
||||
if (options.ignoreGrid === undefined) {
|
||||
options.ignoreGrid = false;
|
||||
}
|
||||
this.dragRulerIgnoreGrid = options.ignoreGrid;
|
||||
// If this is the ruler of a remote user take the waypoints as they were transmitted and don't apply any additional snapping to them
|
||||
if (this.user !== game.user) {
|
||||
options.snap = false;
|
||||
}
|
||||
this.dragRulerSnap = options.snap ?? !disableSnap;
|
||||
this.dragRulerEnableTerrainRuler = isToken && window.terrainRuler;
|
||||
|
||||
// Compute the measurement destination, segments, and distance
|
||||
const d = this._getMeasurementDestination(destination);
|
||||
if (d.x === this.destination.x && d.y === this.destination.y) return;
|
||||
this.destination = d;
|
||||
|
||||
// TODO Check if we can reuse the old path
|
||||
this.dragRulerRemovePathfindingWaypoints();
|
||||
if (this.pathfindingJob) {
|
||||
routinglib.cancelPathfinding(this.pathfindingJob);
|
||||
this.pathfindingJob = undefined;
|
||||
}
|
||||
|
||||
if (isToken && isPathfindingEnabled.call(this)) {
|
||||
// TODO Show a busy indicator
|
||||
const from = getGridPositionFromPixelsObj(this.waypoints[this.waypoints.length - 1]);
|
||||
const to = getGridPositionFromPixelsObj(destination);
|
||||
|
||||
const pathfindingJob = routinglib.calculatePath(from, to, {token: this.draggedEntity});
|
||||
this.pathfindingJob = pathfindingJob;
|
||||
return this.pathfindingJob.then(result => {
|
||||
if (pathfindingJob === this.pathfindingJob) {
|
||||
this.pathfindingJob = undefined;
|
||||
this.addPathToWaypoints(result?.path);
|
||||
return this.performPostPathfindingActions(options);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return this.performPostPathfindingActions(options);
|
||||
}
|
||||
|
||||
addPathToWaypoints(path) {
|
||||
if (!path) {
|
||||
// TODO Show an indicator informing that there is no path
|
||||
|
||||
// Don't show a path if the pathfinding yields no result to show the user that the destination is unreachable
|
||||
this.destination = this.waypoints[this.waypoints.length - 1];
|
||||
return;
|
||||
}
|
||||
|
||||
path = path.map(point =>
|
||||
getSnapPointForTokenObj(getPixelsFromGridPositionObj(point), this.draggedEntity),
|
||||
);
|
||||
|
||||
// If the token is snapped to the grid, the first point of the path is already handled by the ruler
|
||||
if (
|
||||
path[0].x === this.waypoints[this.waypoints.length - 1].x &&
|
||||
path[0].y === this.waypoints[this.waypoints.length - 1].y
|
||||
) {
|
||||
path = path.slice(1);
|
||||
}
|
||||
|
||||
// If snapping is enabled, the last point of the path is already handled by the ruler
|
||||
if (this.dragRulerSnap) {
|
||||
path = path.slice(0, path.length - 1);
|
||||
}
|
||||
|
||||
for (const point of path) {
|
||||
point.isPathfinding = true;
|
||||
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
|
||||
}
|
||||
this.waypoints = this.waypoints.concat(path);
|
||||
}
|
||||
|
||||
performPostPathfindingActions(options) {
|
||||
// TODO Clear pathfinding busy indicator
|
||||
this.segments = this._getMeasurementSegments();
|
||||
this._computeDistance(options.gridSpaces);
|
||||
|
||||
// Draw the ruler graphic
|
||||
this.ruler.clear();
|
||||
this._drawMeasuredPath();
|
||||
|
||||
// Draw grid highlight
|
||||
this.highlightLayer.clear();
|
||||
const isToken = this.draggedEntity instanceof Token;
|
||||
if (isToken && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS && this.dragRulerGridSpaces) {
|
||||
const shape = getTokenShape(this.draggedEntity);
|
||||
if (!this.dragRulerEnableTerrainRuler) {
|
||||
for (const [i, segment] of [...this.segments].reverse().entries()) {
|
||||
const opacityMultiplier = segment.ray.isPrevious ? 0.33 : 1;
|
||||
const previousSegments = this.segments.slice(0, this.segments.length - 1 - i);
|
||||
highlightMeasurementNative.call(
|
||||
this,
|
||||
segment.ray,
|
||||
previousSegments,
|
||||
shape,
|
||||
opacityMultiplier,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for (const segment of [...this.segments].reverse()) {
|
||||
const opacityMultiplier = segment.ray.isPrevious ? 0.33 : 1;
|
||||
highlightMeasurementTerrainRuler.call(
|
||||
this,
|
||||
segment.ray,
|
||||
segment.startDistance,
|
||||
shape,
|
||||
opacityMultiplier,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.segments;
|
||||
}
|
||||
|
||||
_getMeasurementDestination(destination) {
|
||||
if (this.isDragRuler) {
|
||||
if (this.dragRulerSnap) {
|
||||
return getSnapPointForEntity(destination.x, destination.y, this.draggedEntity);
|
||||
} else {
|
||||
return destination;
|
||||
}
|
||||
} else {
|
||||
return super._getMeasurementDestination(destination);
|
||||
}
|
||||
}
|
||||
|
||||
_getMeasurementSegments() {
|
||||
if (this.isDragRuler) {
|
||||
const unsnappedWaypoints = this.waypoints.concat([this.destination]);
|
||||
const waypoints =
|
||||
this.draggedEntity instanceof Token
|
||||
? applyTokenSizeOffset(unsnappedWaypoints, this.draggedEntity)
|
||||
: duplicate(unsnappedWaypoints);
|
||||
const unsnappedSegments = [];
|
||||
const segments = [];
|
||||
for (const [i, p1] of waypoints.entries()) {
|
||||
if (i === 0) continue;
|
||||
const unsnappedP1 = unsnappedWaypoints[i];
|
||||
const p0 = waypoints[i - 1];
|
||||
const unsnappedP0 = unsnappedWaypoints[i - 1];
|
||||
const label = this.labels.children[i - 1];
|
||||
const ray = new Ray(p0, p1);
|
||||
const unsnappedRay = new Ray(unsnappedP0, unsnappedP1);
|
||||
ray.isPrevious = Boolean(unsnappedP0.isPrevious);
|
||||
unsnappedRay.isPrevious = ray.isPrevious;
|
||||
ray.dragRulerVisitedSpaces = unsnappedP0.dragRulerVisitedSpaces;
|
||||
unsnappedRay.dragRulerVisitedSpaces = ray.dragRulerVisitedSpaces;
|
||||
ray.dragRulerFinalState = unsnappedP0.dragRulerFinalState;
|
||||
unsnappedRay.dragRulerFinalState = ray.dragRulerFinalState;
|
||||
if (ray.distance < 10) {
|
||||
if (label) label.visible = false;
|
||||
continue;
|
||||
}
|
||||
segments.push({ray, label});
|
||||
unsnappedSegments.push({ray: unsnappedRay, label});
|
||||
}
|
||||
this.dragRulerUnsnappedSegments = unsnappedSegments;
|
||||
return segments;
|
||||
} else {
|
||||
return super._getMeasurementSegments();
|
||||
}
|
||||
}
|
||||
|
||||
_computeDistance(gridSpaces) {
|
||||
if (!this.isDragRuler) {
|
||||
return super._computeDistance(gridSpaces);
|
||||
}
|
||||
if (!this.dragRulerEnableTerrainRuler) {
|
||||
if (!this.dragRulerIgnoreGrid) {
|
||||
gridSpaces = true;
|
||||
}
|
||||
super._computeDistance(gridSpaces);
|
||||
} else {
|
||||
const shape = this.draggedEntity ? getTokenShape(this.draggedEntity) : null;
|
||||
const options = {
|
||||
ignoreGrid: this.dragRulerIgnoreGrid,
|
||||
gridSpaces,
|
||||
enableTerrainRuler: this.dragRulerEnableTerrainRuler,
|
||||
};
|
||||
const distances = measureDistances(this.segments, this.draggedEntity, shape, options);
|
||||
let totalDistance = 0;
|
||||
for (const [i, d] of distances.entries()) {
|
||||
let s = this.segments[i];
|
||||
s.startDistance = totalDistance;
|
||||
totalDistance += d;
|
||||
s.last = i === this.segments.length - 1;
|
||||
s.distance = d;
|
||||
s.text = this._getSegmentLabel(s, totalDistance);
|
||||
}
|
||||
}
|
||||
for (const [i, segment] of this.segments.entries()) {
|
||||
const unsnappedSegment = this.dragRulerUnsnappedSegments[i];
|
||||
unsnappedSegment.startDistance = segment.startDistance;
|
||||
unsnappedSegment.last = segment.last;
|
||||
unsnappedSegment.distance = segment.distance;
|
||||
unsnappedSegment.text = segment.text;
|
||||
}
|
||||
}
|
||||
|
||||
_drawMeasuredPath() {
|
||||
if (!this.isDragRuler) {
|
||||
return super._drawMeasuredPath();
|
||||
}
|
||||
let rulerColor = this.color;
|
||||
if (!this.dragRulerGridSpaces || canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||
const totalDistance = this.segments.reduce((total, current) => total + current.distance, 0);
|
||||
rulerColor = this.dragRulerGetColorForDistance(totalDistance);
|
||||
}
|
||||
const r = this.ruler.beginFill(rulerColor, 0.25);
|
||||
for (const segment of this.dragRulerUnsnappedSegments) {
|
||||
const opacityMultiplier = segment.ray.isPrevious ? 0.33 : 1;
|
||||
const {ray, distance, label, text, last} = segment;
|
||||
if (distance === 0) continue;
|
||||
|
||||
// Draw Line
|
||||
r.moveTo(ray.A.x, ray.A.y)
|
||||
.lineStyle(6, 0x000000, 0.5 * opacityMultiplier)
|
||||
.lineTo(ray.B.x, ray.B.y)
|
||||
.lineStyle(4, rulerColor, 0.25 * opacityMultiplier)
|
||||
.moveTo(ray.A.x, ray.A.y)
|
||||
.lineTo(ray.B.x, ray.B.y);
|
||||
|
||||
// Draw Waypoints
|
||||
r.lineStyle(2, 0x000000, 0.5).drawCircle(ray.A.x, ray.A.y, 8);
|
||||
if (last) r.drawCircle(ray.B.x, ray.B.y, 8);
|
||||
|
||||
// Draw Label
|
||||
if (label) {
|
||||
label.text = text;
|
||||
label.alpha = last ? 1.0 : 0.5;
|
||||
label.visible = true;
|
||||
let labelPosition = ray.project((ray.distance + 50) / ray.distance);
|
||||
label.position.set(labelPosition.x, labelPosition.y);
|
||||
}
|
||||
}
|
||||
r.endFill();
|
||||
}
|
||||
|
||||
_endMeasurement() {
|
||||
super._endMeasurement();
|
||||
this.draggedEntity = null;
|
||||
if (this.pathfindingJob) {
|
||||
routinglib.cancelPathfinding(this.pathfindingJob);
|
||||
this.pathfindingJob = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// The functions below aren't present in the orignal Ruler class and are added by Drag Ruler
|
||||
dragRulerAddWaypoint(point, options={}) {
|
||||
dragRulerAddWaypoint(point, options = {}) {
|
||||
options.snap = options.snap ?? true;
|
||||
if (options.snap) {
|
||||
point = getSnapPointForEntity(point.x, point.y, this.draggedEntity);
|
||||
}
|
||||
this.waypoints.push(new PIXI.Point(point.x, point.y));
|
||||
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
|
||||
this.waypoints
|
||||
.filter(waypoint => waypoint.isPathfinding)
|
||||
.forEach(waypoint => (waypoint.isPathfinding = false));
|
||||
}
|
||||
|
||||
dragRulerAddWaypointHistory(waypoints) {
|
||||
waypoints.forEach(waypoint => waypoint.isPrevious = true);
|
||||
waypoints.forEach(waypoint => (waypoint.isPrevious = true));
|
||||
this.waypoints = this.waypoints.concat(waypoints);
|
||||
for (const waypoint of waypoints) {
|
||||
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
|
||||
@@ -90,24 +370,51 @@ export function extendRuler() {
|
||||
this.labels.removeChildren().forEach(c => c.destroy());
|
||||
}
|
||||
|
||||
dragRulerDeleteWaypoint(event={preventDefault: () => {return}}, options={}) {
|
||||
dragRulerDeleteWaypoint(
|
||||
event = {
|
||||
preventDefault: () => {
|
||||
return;
|
||||
},
|
||||
},
|
||||
options = {},
|
||||
) {
|
||||
this.dragRulerRemovePathfindingWaypoints();
|
||||
options.snap = options.snap ?? true;
|
||||
if (this.waypoints.filter(w => !w.isPrevious).length > 1) {
|
||||
event.preventDefault();
|
||||
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens);
|
||||
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(
|
||||
canvas.tokens,
|
||||
);
|
||||
const rulerOffset = this.rulerOffset;
|
||||
|
||||
// Options are not passed to _removeWaypoint in vanilla Foundry.
|
||||
// Send them in case other modules have overriden that behavior and accept an options parameter (Toggle Snap to Grid)
|
||||
this._removeWaypoint({x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y}, options);
|
||||
this._removeWaypoint(
|
||||
{x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y},
|
||||
options,
|
||||
);
|
||||
game.user.broadcastActivity({ruler: this});
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.dragRulerAbortDrag(event);
|
||||
}
|
||||
}
|
||||
|
||||
dragRulerAbortDrag(event={preventDefault: () => {return}}) {
|
||||
dragRulerRemovePathfindingWaypoints() {
|
||||
this.waypoints
|
||||
.filter(waypoint => waypoint.isPathfinding)
|
||||
.forEach(_ =>
|
||||
this.labels.removeChild(this.labels.children[this.labels.children.length - 1]).destroy(),
|
||||
);
|
||||
this.waypoints = this.waypoints.filter(waypoint => !waypoint.isPathfinding);
|
||||
}
|
||||
|
||||
dragRulerAbortDrag(
|
||||
event = {
|
||||
preventDefault: () => {
|
||||
return;
|
||||
},
|
||||
},
|
||||
) {
|
||||
const token = this.draggedEntity;
|
||||
this._endMeasurement();
|
||||
|
||||
@@ -121,10 +428,8 @@ export function extendRuler() {
|
||||
}
|
||||
|
||||
async dragRulerRecalculate(tokenIds) {
|
||||
if (this._state !== Ruler.STATES.MEASURING)
|
||||
return;
|
||||
if (tokenIds && !tokenIds.includes(this.draggedEntity.id))
|
||||
return;
|
||||
if (this._state !== Ruler.STATES.MEASURING) return;
|
||||
if (tokenIds && !tokenIds.includes(this.draggedEntity.id)) return;
|
||||
const waypoints = this.waypoints.filter(waypoint => !waypoint.isPrevious);
|
||||
this.dragRulerClearWaypoints();
|
||||
if (game.settings.get(settingsKey, "enableMovementHistory"))
|
||||
@@ -137,55 +442,74 @@ export function extendRuler() {
|
||||
}
|
||||
|
||||
static dragRulerGetRaysFromWaypoints(waypoints, destination) {
|
||||
if ( destination )
|
||||
waypoints = waypoints.concat([destination]);
|
||||
if (destination) waypoints = waypoints.concat([destination]);
|
||||
return waypoints.slice(1).map((wp, i) => {
|
||||
const ray = new Ray(waypoints[i], wp);
|
||||
const ray = new Ray(waypoints[i], wp);
|
||||
ray.isPrevious = Boolean(waypoints[i].isPrevious);
|
||||
return ray;
|
||||
});
|
||||
}
|
||||
|
||||
dragRulerGetColorForDistance(distance) {
|
||||
if (!this.isDragRuler)
|
||||
return this.color;
|
||||
if (!this.isDragRuler) return this.color;
|
||||
if (!this.draggedEntity.actor) {
|
||||
return this.color;
|
||||
}
|
||||
// Don't apply colors if the current user doesn't have at least observer permissions
|
||||
if (this.draggedEntity.actor.permission < 2) {
|
||||
// If this is a pc and alwaysShowSpeedForPCs is enabled we show the color anyway
|
||||
if (!(this.draggedEntity.actor.data.type === "character" && game.settings.get(settingsKey, "alwaysShowSpeedForPCs")))
|
||||
if (
|
||||
!(
|
||||
this.draggedEntity.actor.data.type === "character" &&
|
||||
game.settings.get(settingsKey, "alwaysShowSpeedForPCs")
|
||||
)
|
||||
)
|
||||
return this.color;
|
||||
}
|
||||
distance = Math.round(distance * 100) / 100;
|
||||
if (!this.dragRulerRanges)
|
||||
this.dragRulerRanges = getRangesFromSpeedProvider(this.draggedEntity);
|
||||
return getColorForDistanceAndToken(distance, this.draggedEntity, this.dragRulerRanges) ?? this.color;
|
||||
return (
|
||||
getColorForDistanceAndToken(distance, this.draggedEntity, this.dragRulerRanges) ??
|
||||
this.color
|
||||
);
|
||||
}
|
||||
|
||||
dragRulerStart(options, measureImmediately=true) {
|
||||
dragRulerStart(options, measureImmediately = true) {
|
||||
const entity = this.draggedEntity;
|
||||
const isToken = entity instanceof Token;
|
||||
if (isToken && !currentSpeedProvider.usesRuler(entity))
|
||||
return;
|
||||
if (isToken && !currentSpeedProvider.usesRuler(entity)) return;
|
||||
const ruler = canvas.controls.ruler;
|
||||
ruler.clear();
|
||||
ruler._state = Ruler.STATES.STARTING;
|
||||
let entityCenter;
|
||||
if (isToken && canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(entity))
|
||||
if (
|
||||
isToken &&
|
||||
canvas.grid.isHex &&
|
||||
game.modules.get("hex-size-support")?.active &&
|
||||
CONFIG.hexSizeSupport.getAltSnappingFlag(entity)
|
||||
)
|
||||
entityCenter = getHexSizeSupportTokenGridCenter(entity);
|
||||
else
|
||||
entityCenter = entity.center;
|
||||
else entityCenter = entity.center;
|
||||
if (isToken && game.settings.get(settingsKey, "enableMovementHistory"))
|
||||
ruler.dragRulerAddWaypointHistory(getMovementHistory(entity));
|
||||
ruler.dragRulerAddWaypoint(entityCenter, {snap: false});
|
||||
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens);
|
||||
const destination = {x: mousePosition.x + ruler.rulerOffset.x, y: mousePosition.y + ruler.rulerOffset.y};
|
||||
if (measureImmediately)
|
||||
ruler.measure(destination, options);
|
||||
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(
|
||||
canvas.tokens,
|
||||
);
|
||||
const destination = {
|
||||
x: mousePosition.x + ruler.rulerOffset.x,
|
||||
y: mousePosition.y + ruler.rulerOffset.y,
|
||||
};
|
||||
if (measureImmediately) ruler.measure(destination, options);
|
||||
}
|
||||
|
||||
dragRulerSendState() {
|
||||
game.user.broadcastActivity({
|
||||
ruler: this.toJSON(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ruler = DragRulerRuler;
|
||||
CONFIG.Canvas.rulerClass = DragRulerRuler;
|
||||
}
|
||||
|
||||
+129
-93
@@ -1,27 +1,37 @@
|
||||
import {availableSpeedProviders, currentSpeedProvider, getDefaultSpeedProvider, updateSpeedProvider} from "./api.js";
|
||||
import {SpeedProvider} from "./speed_provider.js"
|
||||
import {
|
||||
availableSpeedProviders,
|
||||
currentSpeedProvider,
|
||||
getDefaultSpeedProvider,
|
||||
updateSpeedProvider,
|
||||
} from "./api.js";
|
||||
import {SpeedProvider} from "./speed_provider.js";
|
||||
import {early_isGM} from "./util.js";
|
||||
|
||||
export const settingsKey = "drag-ruler";
|
||||
|
||||
export const RightClickAction = Object.freeze({
|
||||
CREATE_WAYPOINT: 0,
|
||||
DELETE_WAYPOINT: 1,
|
||||
ABORT_DRAG:2,
|
||||
ABORT_DRAG: 2,
|
||||
});
|
||||
|
||||
function delayedReload() {
|
||||
window.setTimeout(() => location.reload(), 500);
|
||||
}
|
||||
|
||||
export function registerSettings() {
|
||||
game.settings.register(settingsKey, "dataVersion", {
|
||||
scope: "world",
|
||||
config: false,
|
||||
type: String,
|
||||
default: "fresh install"
|
||||
})
|
||||
default: "fresh install",
|
||||
});
|
||||
|
||||
game.settings.register(settingsKey, "clientDataVersion", {
|
||||
scope: "client",
|
||||
config: false,
|
||||
type: String,
|
||||
default: "fresh install"
|
||||
default: "fresh install",
|
||||
});
|
||||
|
||||
game.settings.register(settingsKey, "rightClickAction", {
|
||||
@@ -62,7 +72,7 @@ export function registerSettings() {
|
||||
config: true,
|
||||
type: Boolean,
|
||||
default: true,
|
||||
})
|
||||
});
|
||||
|
||||
game.settings.register(settingsKey, "showGMRulerToPlayers", {
|
||||
name: "drag-ruler.settings.showGMRulerToPlayers.name",
|
||||
@@ -71,7 +81,7 @@ export function registerSettings() {
|
||||
config: true,
|
||||
type: Boolean,
|
||||
default: true,
|
||||
})
|
||||
});
|
||||
|
||||
game.settings.register(settingsKey, "enableMovementHistory", {
|
||||
name: "drag-ruler.settings.enableMovementHistory.name",
|
||||
@@ -82,6 +92,27 @@ export function registerSettings() {
|
||||
default: true,
|
||||
});
|
||||
|
||||
if (game.modules.get("routinglib")?.active) {
|
||||
game.settings.register(settingsKey, "allowPathfinding", {
|
||||
name: "drag-ruler.settings.allowPathfinding.name",
|
||||
hint: "drag-ruler.settings.allowPathfinding.hint",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
onChange: delayedReload,
|
||||
});
|
||||
|
||||
game.settings.register(settingsKey, "autoPathfinding", {
|
||||
name: "drag-ruler.settings.autoPathfinding.name",
|
||||
hint: "drag-ruler.settings.autoPathfinding.hint",
|
||||
scope: "client",
|
||||
config: early_isGM() || game.settings.get(settingsKey, "allowPathfinding"),
|
||||
type: Boolean,
|
||||
default: false,
|
||||
});
|
||||
}
|
||||
|
||||
game.settings.register(settingsKey, "lastTerrainRulerHintTime", {
|
||||
config: false,
|
||||
type: Number,
|
||||
@@ -101,7 +132,7 @@ export function registerSettings() {
|
||||
type: String,
|
||||
default: getDefaultSpeedProvider(),
|
||||
onChange: updateSpeedProvider,
|
||||
})
|
||||
});
|
||||
|
||||
game.settings.registerMenu(settingsKey, "speedProviderSettings", {
|
||||
name: "drag-ruler.settings.speedProviderSettings.name",
|
||||
@@ -110,7 +141,7 @@ export function registerSettings() {
|
||||
icon: "fas fa-tachometer-alt",
|
||||
type: SpeedProviderSettings,
|
||||
restricted: false,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
class SpeedProviderSettings extends FormApplication {
|
||||
@@ -120,43 +151,44 @@ class SpeedProviderSettings extends FormApplication {
|
||||
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
|
||||
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)
|
||||
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
|
||||
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
|
||||
name = game.modules.get(id).data.title;
|
||||
} else {
|
||||
name = game.system.data.title;
|
||||
}
|
||||
else {
|
||||
name = game.system.data.title
|
||||
}
|
||||
provider.selectTitle = game.i18n.format(`drag-ruler.settings.speedProviderSettings.speedProvider.choices.${type}`, {name})
|
||||
provider.selectTitle = game.i18n.format(
|
||||
`drag-ruler.settings.speedProviderSettings.speedProvider.choices.${type}`,
|
||||
{name},
|
||||
);
|
||||
}
|
||||
provider.isSelected = provider.id === selectedProvider
|
||||
return provider
|
||||
})
|
||||
data.selectedProviderName = data.providers.find(provider => provider.isSelected).selectTitle
|
||||
provider.isSelected = provider.id === selectedProvider;
|
||||
return provider;
|
||||
});
|
||||
data.selectedProviderName = data.providers.find(provider => provider.isSelected).selectTitle;
|
||||
|
||||
data.providerSelection = {
|
||||
id: "speedProvider",
|
||||
@@ -164,122 +196,126 @@ class SpeedProviderSettings extends FormApplication {
|
||||
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
|
||||
choices[provider.id] = provider.selectTitle;
|
||||
return choices;
|
||||
}, {}),
|
||||
value: selectedProvider,
|
||||
isCheckbox: false,
|
||||
isSelect: true,
|
||||
isRange: false,
|
||||
}
|
||||
return data
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
async _updateObject(event, formData) {
|
||||
const selectedSpeedProvider = game.user.isGM ? formData.speedProvider : game.settings.get(settingsKey, "speedProvider")
|
||||
const selectedSpeedProvider = game.user.isGM
|
||||
? formData.speedProvider
|
||||
: game.settings.get(settingsKey, "speedProvider");
|
||||
for (let [key, value] of Object.entries(formData)) {
|
||||
// Check if this is color, convert the value to an integer
|
||||
const splitKey = key.split(".", 3)
|
||||
if (splitKey[0] !== "native")
|
||||
splitKey.shift()
|
||||
const splitKey = key.split(".", 3);
|
||||
if (splitKey[0] !== "native") splitKey.shift();
|
||||
if (splitKey.length >= 2 && splitKey[1] == "color") {
|
||||
value = parseInt(value.substring(1), 16)
|
||||
value = parseInt(value.substring(1), 16);
|
||||
}
|
||||
|
||||
// Don't change settings for speed providers that aren't currently active
|
||||
if (key !== "speedProvider" && !key.startsWith(selectedSpeedProvider))
|
||||
continue
|
||||
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}`
|
||||
let setting;
|
||||
if (key === "speedProvider") setting = "speedProvider";
|
||||
else setting = `speedProviders.${key}`;
|
||||
|
||||
// Get the old setting value
|
||||
const oldValue = game.settings.get(settingsKey, setting)
|
||||
const oldValue = game.settings.get(settingsKey, setting);
|
||||
|
||||
// Only update the setting if it has been changed (this leaves the default in place if it hasn't been touched)
|
||||
if (value !== oldValue)
|
||||
game.settings.set(settingsKey, setting, value)
|
||||
if (value !== oldValue) game.settings.set(settingsKey, setting, value);
|
||||
}
|
||||
|
||||
// Activate the configured speed provider
|
||||
updateSpeedProvider()
|
||||
updateSpeedProvider();
|
||||
}
|
||||
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html)
|
||||
html.find("select[name=speedProvider]").change(this.onSpeedProviderChange.bind(this))
|
||||
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")
|
||||
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 = ""
|
||||
document.getElementById(`drag-ruler.provider.${event.currentTarget.value}`).style.display = "";
|
||||
|
||||
// Recalculate window height
|
||||
this.element[0].style.height = null
|
||||
this.position.height = undefined
|
||||
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
|
||||
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"}
|
||||
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
|
||||
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")
|
||||
hint = game.i18n.localize("drag-ruler.settings.speedProviderSettings.color.unreachable.hint");
|
||||
else
|
||||
hint = game.i18n.format("drag-ruler.settings.speedProviderSettings.color.hint", {colorName})
|
||||
hint = game.i18n.format("drag-ruler.settings.speedProviderSettings.color.hint", {colorName});
|
||||
colorSettings.push({
|
||||
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}`)),
|
||||
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 = []
|
||||
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)
|
||||
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)
|
||||
return settings.concat(colorSettings);
|
||||
}
|
||||
|
||||
+15
-6
@@ -11,8 +11,13 @@ Hooks.once("socketlib.ready", () => {
|
||||
export function updateCombatantDragRulerFlags(combat, updates) {
|
||||
const combatId = combat.id;
|
||||
// TODO Check if canvas.tokens.get is still neccessary in future foundry versions
|
||||
return socket.executeAsGM(_socketUpdateCombatantDragRulerFlags, combatId, updates)
|
||||
.then(() => currentSpeedProvider.onMovementHistoryUpdate(updates.map(update => canvas.tokens.get(combat.combatants.get(update._id).token.id))));
|
||||
return socket
|
||||
.executeAsGM(_socketUpdateCombatantDragRulerFlags, combatId, updates)
|
||||
.then(() =>
|
||||
currentSpeedProvider.onMovementHistoryUpdate(
|
||||
updates.map(update => canvas.tokens.get(combat.combatants.get(update._id).token.id)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function _socketUpdateCombatantDragRulerFlags(combatId, updates) {
|
||||
@@ -21,12 +26,15 @@ async function _socketUpdateCombatantDragRulerFlags(combatId, updates) {
|
||||
const requestedUpdates = updates.length;
|
||||
updates = updates.filter(update => {
|
||||
const actor = combat.combatants.get(update._id).actor;
|
||||
if (!actor)
|
||||
return false;
|
||||
if (!actor) return false;
|
||||
return actor.testUserPermission(user, "OWNER");
|
||||
});
|
||||
if (updates.length !== requestedUpdates) {
|
||||
console.warn(`Some of the movement history updates requested by user '${game.users.get(this.socketdata.userId).name}' were not performed because the user lacks owner permissions for those tokens`);
|
||||
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}};
|
||||
@@ -39,5 +47,6 @@ export function recalculate(tokens) {
|
||||
}
|
||||
|
||||
function _socketRecalculate(tokenIds) {
|
||||
return canvas.controls.ruler.dragRulerRecalculate(tokenIds);
|
||||
const ruler = canvas.controls.ruler;
|
||||
if (ruler.isDragRuler) ruler.dragRulerRecalculate(tokenIds);
|
||||
}
|
||||
|
||||
+31
-49
@@ -1,5 +1,5 @@
|
||||
import {settingsKey} from "./settings.js"
|
||||
import {getDefaultDashMultiplier, getDefaultSpeedAttribute} from "./systems.js"
|
||||
import {settingsKey} from "./settings.js";
|
||||
import {getDefaultDashMultiplier, getDefaultSpeedAttribute} from "./systems.js";
|
||||
|
||||
/**
|
||||
* Base class for all speed providers.
|
||||
@@ -19,7 +19,7 @@ export class SpeedProvider {
|
||||
* Implementing this method is required for all speed providers
|
||||
*/
|
||||
get colors() {
|
||||
throw new Error("A SpeedProvider must implement the colors function")
|
||||
throw new Error("A SpeedProvider must implement the colors function");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,7 +31,7 @@ export class SpeedProvider {
|
||||
* Implementing this method is required for all speed providers
|
||||
*/
|
||||
getRanges(token) {
|
||||
throw new Error("A SpeedProvider must implement the getRanges function")
|
||||
throw new Error("A SpeedProvider must implement the getRanges function");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,7 +44,7 @@ export class SpeedProvider {
|
||||
* Implementing this method is optional and only needs to be done if you want to provide custom provider settings
|
||||
*/
|
||||
get settings() {
|
||||
return []
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,28 +53,7 @@ export class SpeedProvider {
|
||||
* Implementing this method is optional and only needs to be done if you want to provide a custom default for that color.
|
||||
*/
|
||||
get defaultUnreachableColor() {
|
||||
return 0xFF0000
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cost for a token to step into the specificed area.
|
||||
* The area indicates the whole area that the token will occupy (for tokens larger than 1x1) the array will more than one entry.
|
||||
* The return value should be an integer indicating a multiplicator by that the cost of that step should be increased.
|
||||
* (1 is regular cost, 2 costs double, 3 costs triple, ...)
|
||||
*
|
||||
* Parameters:
|
||||
* - options: An object used to configure Enhanced Terrain Layer's cost calculation. Ex: If options.ignoreGrid is set to true, then Euclidean measurement can be forced on a gridded map.
|
||||
*
|
||||
* This function is only called if the Enhanced Terrain Layer and Terrain Ruler modules are enabled.
|
||||
*
|
||||
* Implementing this method is optional and only needs to be done if you want to provide a custom cost function (for example to allow tokens to ignore difficult terrain)
|
||||
*/
|
||||
getCostForStep(token, area, options={}) {
|
||||
// Lookup the cost for each square occupied by the token
|
||||
options.token = token;
|
||||
const costs = area.map(space => terrainRuler.getCost(space.x, space.y, options));
|
||||
// Return the maximum of the costs
|
||||
return costs.reduce((max, current) => Math.max(max, current))
|
||||
return 0xff0000;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,7 +65,7 @@ export class SpeedProvider {
|
||||
* Implementing this method is optional and only needs to be done if you want to disable Drag Ruler for some tokens.
|
||||
*/
|
||||
usesRuler(token) {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,13 +83,14 @@ export class SpeedProvider {
|
||||
*/
|
||||
getSetting(settingId) {
|
||||
try {
|
||||
return game.settings.get(settingsKey, `speedProviders.${this.id}.setting.${settingId}`)
|
||||
}
|
||||
catch (e) {
|
||||
return game.settings.get(settingsKey, `speedProviders.${this.id}.setting.${settingId}`);
|
||||
} catch (e) {
|
||||
if (this.settings.some(setting => setting.id === settingId)) {
|
||||
throw e
|
||||
throw e;
|
||||
}
|
||||
throw new Error(`Drag Ruler | "${settingId}" is not a registered setting for "${this.id}". If you're the module/system developer, please add it to the return values of your Speed Providers "get settings()" function.`)
|
||||
throw new Error(
|
||||
`Drag Ruler | "${settingId}" is not a registered setting for "${this.id}". If you're the module/system developer, please add it to the return values of your Speed Providers "get settings()" function.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,32 +100,34 @@ export class SpeedProvider {
|
||||
* This function should neither be called or overridden by speed provider implementations
|
||||
*/
|
||||
constructor(id) {
|
||||
this.id = 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"}
|
||||
]
|
||||
{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 speedAttribute = this.getSetting("speedAttribute");
|
||||
if (!speedAttribute) return [];
|
||||
const tokenSpeed = parseFloat(getProperty(token, speedAttribute));
|
||||
if (tokenSpeed === undefined) {
|
||||
console.warn(`Drag Ruler (Generic Speed Provider) | The configured token speed attribute "${speedAttribute}" didn't return a speed value. To use colors based on drag distance set the setting to the correct value (or clear the box to disable this feature).`)
|
||||
return []
|
||||
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"}]
|
||||
const dashMultiplier = this.getSetting("dashMultiplier");
|
||||
if (!dashMultiplier) return [{range: tokenSpeed, color: "walk"}];
|
||||
return [
|
||||
{range: tokenSpeed, color: "walk"},
|
||||
{range: tokenSpeed * dashMultiplier, color: "dash"},
|
||||
];
|
||||
}
|
||||
|
||||
get settings() {
|
||||
@@ -167,7 +149,7 @@ export class GenericSpeedProvider extends SpeedProvider {
|
||||
config: true,
|
||||
type: Number,
|
||||
default: getDefaultDashMultiplier(),
|
||||
}
|
||||
]
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+22
-12
@@ -1,40 +1,50 @@
|
||||
export function getDefaultSpeedAttribute() {
|
||||
switch (game.system.id) {
|
||||
case "CoC7":
|
||||
return "actor.data.data.attribs.mov.value";
|
||||
return "actor.system.attribs.mov.value";
|
||||
case "dcc":
|
||||
return "actor.data.data.attributes.speed.value";
|
||||
return "actor.system.attributes.speed.value";
|
||||
case "dnd4e":
|
||||
return "actor.system.movement.walk.value";
|
||||
case "dnd5e":
|
||||
return "actor.data.data.attributes.movement.walk"
|
||||
return "actor.system.attributes.movement.walk";
|
||||
case "lancer":
|
||||
return "actor.data.data.derived.speed"
|
||||
return "actor.system.derived.speed";
|
||||
case "pf1":
|
||||
case "D35E":
|
||||
return "actor.data.data.attributes.speed.land.total"
|
||||
return "actor.system.attributes.speed.land.total";
|
||||
case "sfrpg":
|
||||
return "actor.data.data.attributes.speed.value";
|
||||
return "actor.system.attributes.speed.value";
|
||||
case "shadowrun5e":
|
||||
return "actor.data.data.movement.walk.value";
|
||||
return "actor.system.movement.walk.value";
|
||||
case "swade":
|
||||
return "actor.data.data.stats.speed.adjusted";
|
||||
return "actor.system.stats.speed.adjusted";
|
||||
case "ds4":
|
||||
return "actor.system.combatValues.movement.total";
|
||||
case "splittermond":
|
||||
return "actor.derivedValues.speed.value";
|
||||
}
|
||||
return ""
|
||||
return "";
|
||||
}
|
||||
|
||||
export function getDefaultDashMultiplier() {
|
||||
switch (game.system.id) {
|
||||
case "swade":
|
||||
return 0
|
||||
return 0;
|
||||
case "dcc":
|
||||
case "dnd4e":
|
||||
case "dnd5e":
|
||||
case "lancer":
|
||||
case "pf1":
|
||||
case "D35E":
|
||||
case "sfrpg":
|
||||
case "shadowrun5e":
|
||||
return 2
|
||||
case "ds4":
|
||||
return 2;
|
||||
case "CoC7":
|
||||
return 5;
|
||||
case "splittermond":
|
||||
return 3;
|
||||
}
|
||||
return 0
|
||||
return 0;
|
||||
}
|
||||
|
||||
+140
-100
@@ -1,9 +1,11 @@
|
||||
import {getPixelsFromGridPosition} from "./foundry_fixes.js"
|
||||
import { disableSnap } from "./keybindings.js";
|
||||
import {getPixelsFromGridPosition} from "./foundry_fixes.js";
|
||||
import {findVertexSnapPoint} from "./hex_support.js";
|
||||
import {disableSnap, moveWithoutAnimation, togglePathfinding} from "./keybindings.js";
|
||||
import {settingsKey} from "./settings.js";
|
||||
|
||||
export function* zip(it1, it2) {
|
||||
for (let i = 0;i < Math.min(it1.length, it2.length);i++) {
|
||||
yield [it1[i], it2[i]]
|
||||
for (let i = 0; i < Math.min(it1.length, it2.length); i++) {
|
||||
yield [it1[i], it2[i]];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,106 +17,115 @@ export function* enumeratedZip(it1, it2) {
|
||||
}
|
||||
}
|
||||
|
||||
export function* iterPairs(l) {
|
||||
for (let i = 1; i < l.length; i++) {
|
||||
yield [l[i - 1], l[i]];
|
||||
}
|
||||
}
|
||||
|
||||
export function sum(arr) {
|
||||
return arr.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
||||
export function buildSnapPointTokenData(token) {
|
||||
const tokenData = {
|
||||
width: token.document.width,
|
||||
height: token.document.height,
|
||||
};
|
||||
|
||||
if (isModuleActive("hex-size-support")) {
|
||||
tokenData.hexSizeSupport = {};
|
||||
tokenData.hexSizeSupport.altSnappingFlag = CONFIG.hexSizeSupport.getAltSnappingFlag(token);
|
||||
tokenData.hexSizeSupport.altOrientationFlag =
|
||||
CONFIG.hexSizeSupport.getAltOrientationFlag(token);
|
||||
tokenData.hexSizeSupport.borderSize = token.document.getFlag("hex-size-support", "borderSize");
|
||||
}
|
||||
|
||||
return tokenData;
|
||||
}
|
||||
|
||||
export function getSnapPointForToken(x, y, token) {
|
||||
return getSnapPointForTokenData(x, y, buildSnapPointTokenData(token));
|
||||
}
|
||||
|
||||
export function getSnapPointForTokenDataObj(pos, tokenData) {
|
||||
return getSnapPointForTokenData(pos.x, pos.y, tokenData);
|
||||
}
|
||||
|
||||
// A copy of this function lives in the librouting module
|
||||
function getSnapPointForTokenData(x, y, tokenData) {
|
||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||
return new PIXI.Point(x, y);
|
||||
}
|
||||
if (canvas.grid.isHex) {
|
||||
if (game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) {
|
||||
if (token.document.getFlag("hex-size-support", "borderSize") % 2 === 0) {
|
||||
const snapPoint = CONFIG.hexSizeSupport.findVertexSnapPoint(x, y, token, canvas.grid.grid)
|
||||
return new PIXI.Point(snapPoint.x, snapPoint.y)
|
||||
if (tokenData.hexSizeSupport?.altSnappingFlag) {
|
||||
if (tokenData.hexSizeSupport.borderSize % 2 === 0) {
|
||||
const snapPoint = findVertexSnapPoint(x, y, tokenData.hexSizeSupport.altOrientationFlag);
|
||||
return new PIXI.Point(snapPoint.x, snapPoint.y);
|
||||
} else {
|
||||
return new PIXI.Point(...canvas.grid.getCenter(x, y));
|
||||
}
|
||||
else {
|
||||
return new PIXI.Point(...canvas.grid.getCenter(x, y))
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return new PIXI.Point(...canvas.grid.getCenter(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
const [topLeftX, topLeftY] = canvas.grid.getTopLeft(x, y);
|
||||
let cellX, cellY;
|
||||
if (token.data.width % 2 === 0)
|
||||
cellX = x - canvas.grid.h / 2;
|
||||
else
|
||||
cellX = x;
|
||||
if (token.data.height % 2 === 0)
|
||||
cellY = y - canvas.grid.h / 2;
|
||||
else
|
||||
cellY = y;
|
||||
if (tokenData.width % 2 === 0) cellX = x - canvas.grid.h / 2;
|
||||
else cellX = x;
|
||||
if (tokenData.height % 2 === 0) cellY = y - canvas.grid.h / 2;
|
||||
else cellY = y;
|
||||
const [centerX, centerY] = canvas.grid.getCenter(cellX, cellY);
|
||||
let snapX, snapY;
|
||||
// Tiny tokens can snap to the cells corners
|
||||
if (token.data.width <= 0.5) {
|
||||
if (tokenData.width <= 0.5) {
|
||||
const offsetX = x - topLeftX;
|
||||
const subGridWidth = Math.floor(canvas.grid.w / 2);
|
||||
const subGridPosX = Math.floor(offsetX / subGridWidth);
|
||||
snapX = topLeftX + (subGridPosX + 0.5) * subGridWidth;
|
||||
}
|
||||
// Tokens with odd multipliers (1x1, 3x3, ...) and tokens smaller than 1x1 but bigger than 0.5x0.5 snap to the center of the grid cell
|
||||
else if (Math.round(token.data.width) % 2 === 1 || token.data.width < 1) {
|
||||
else if (Math.round(tokenData.width) % 2 === 1 || tokenData.width < 1) {
|
||||
snapX = centerX;
|
||||
}
|
||||
// All remaining tokens (those with even or fractional multipliers on square grids) snap to the intersection points of the grid
|
||||
else {
|
||||
snapX = centerX + canvas.grid.w / 2;
|
||||
}
|
||||
if (token.data.height <= 0.5) {
|
||||
if (tokenData.height <= 0.5) {
|
||||
const offsetY = y - topLeftY;
|
||||
const subGridHeight = Math.floor(canvas.grid.h / 2);
|
||||
const subGridPosY = Math.floor(offsetY / subGridHeight);
|
||||
snapY = topLeftY + (subGridPosY + 0.5) * subGridHeight;
|
||||
}
|
||||
else if (Math.round(token.data.height) % 2 === 1 || token.data.height < 1) {
|
||||
} else if (Math.round(tokenData.height) % 2 === 1 || tokenData.height < 1) {
|
||||
snapY = centerY;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
snapY = centerY + canvas.grid.h / 2;
|
||||
}
|
||||
return new PIXI.Point(snapX, snapY);
|
||||
}
|
||||
|
||||
export function getSnapPointForTokenObj(pos, token) {
|
||||
return getSnapPointForToken(pos.x, pos.y, token);
|
||||
}
|
||||
|
||||
export function getSnapPointForMeasuredTemplate(x, y) {
|
||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||
return new PIXI.Point(x, y);
|
||||
}
|
||||
let subgridWidth, subgridHeight;
|
||||
if (canvas.grid.type === CONST.GRID_TYPES.SQUARE) {
|
||||
subgridWidth = subgridHeight = canvas.dimensions.size / 2;
|
||||
}
|
||||
else {
|
||||
if (canvas.grid.grid.columns) {
|
||||
subgridWidth = canvas.grid.w / 4;
|
||||
subgridHeight = canvas.grid.h / 2;
|
||||
}
|
||||
else {
|
||||
subgridWidth = canvas.grid.w / 2;
|
||||
subgridHeight = canvas.grid.h / 4;
|
||||
}
|
||||
}
|
||||
const snappedX = Math.round(x / subgridWidth) * subgridWidth;
|
||||
const snappedY = Math.round(y / subgridHeight) * subgridHeight;
|
||||
return new PIXI.Point(snappedX, snappedY);
|
||||
return canvas.grid.grid.getSnappedPosition(x, y, canvas.templates.gridPrecision);
|
||||
}
|
||||
|
||||
export function getSnapPointForEntity(x, y, entity) {
|
||||
const isToken = entity instanceof Token;
|
||||
if (isToken)
|
||||
return getSnapPointForToken(x, y, entity);
|
||||
else
|
||||
return getSnapPointForMeasuredTemplate(x, y);
|
||||
if (isToken) return getSnapPointForToken(x, y, entity);
|
||||
else return getSnapPointForMeasuredTemplate(x, y);
|
||||
}
|
||||
|
||||
export function highlightTokenShape(position, shape, color, alpha) {
|
||||
const layer = canvas.grid.highlightLayers[this.name];
|
||||
if ( !layer )
|
||||
return false;
|
||||
if (!layer) return false;
|
||||
const area = getAreaFromPositionAndShape(position, shape);
|
||||
for (const space of area) {
|
||||
const [x, y] = getPixelsFromGridPosition(space.x, space.y);
|
||||
@@ -128,59 +139,73 @@ export function getAreaFromPositionAndShape(position, shape) {
|
||||
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.even) shiftedRow = 1;
|
||||
else shiftedRow = 0;
|
||||
if (canvas.grid.grid.options.columns) {
|
||||
if (space.x % 2 !== 0 && position.x % 2 !== shiftedRow) {
|
||||
y += 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (space.y % 2 !== 0 && position.y % 2 !== shiftedRow) {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {x, y}
|
||||
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 getTokenShapeForTokenData(buildSnapPointTokenData(token), token.scene);
|
||||
}
|
||||
|
||||
export function getTokenShapeForTokenData(tokenData, scene = canvas.scene) {
|
||||
if (scene.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||
return [{x: 0, y: 0}];
|
||||
} else if (scene.grid.type === CONST.GRID_TYPES.SQUARE) {
|
||||
const topOffset = -Math.floor(tokenData.height / 2);
|
||||
const leftOffset = -Math.floor(tokenData.width / 2);
|
||||
const shape = [];
|
||||
for (let y = 0; y < tokenData.height; y++) {
|
||||
for (let x = 0; x < tokenData.width; x++) {
|
||||
shape.push({x: x + leftOffset, y: y + topOffset});
|
||||
}
|
||||
}
|
||||
return shape
|
||||
}
|
||||
else {
|
||||
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;
|
||||
if (game.modules.get("hex-size-support")?.active && tokenData.hexSizeSupport.altSnappingFlag) {
|
||||
const borderSize = tokenData.hexSizeSupport.borderSize;
|
||||
let shape = [{x: 0, y: 0}];
|
||||
if (borderSize >= 2)
|
||||
shape = shape.concat([{x: 0, y: -1}, {x: -1, y: -1}]);
|
||||
shape = shape.concat([
|
||||
{x: 0, y: -1},
|
||||
{x: -1, y: -1},
|
||||
]);
|
||||
if (borderSize >= 3)
|
||||
shape = shape.concat([{x: 0, y: 1}, {x: -1, y: 1}, {x: -1, y: 0}, {x: 1, y: 0}]);
|
||||
shape = shape.concat([
|
||||
{x: 0, y: 1},
|
||||
{x: -1, y: 1},
|
||||
{x: -1, y: 0},
|
||||
{x: 1, y: 0},
|
||||
]);
|
||||
if (borderSize >= 4)
|
||||
shape = shape.concat([{x: -2, y: -1}, {x: 1, y: -1}, {x: -1, y: -2}, {x: 0, y: -2}, {x: 1, y: -2}])
|
||||
shape = shape.concat([
|
||||
{x: -2, y: -1},
|
||||
{x: 1, y: -1},
|
||||
{x: -1, y: -2},
|
||||
{x: 0, y: -2},
|
||||
{x: 1, y: -2},
|
||||
]);
|
||||
|
||||
if (Boolean(CONFIG.hexSizeSupport.getAltOrientationFlag(token)) !== canvas.grid.grid.options.columns)
|
||||
shape.forEach(space => space.y *= -1);
|
||||
if (Boolean(tokenData.hexSizeSupport.altOrientationFlag) !== 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}});
|
||||
shape = shape.map(space => {
|
||||
return {x: space.y, y: space.x};
|
||||
});
|
||||
return shape;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return [{x: 0, y: 0}];
|
||||
}
|
||||
}
|
||||
@@ -188,13 +213,12 @@ export function getTokenShape(token) {
|
||||
|
||||
export function getTokenSize(token) {
|
||||
let w, h;
|
||||
const hexSizeSupportBorderSize = token.data.flags["hex-size-support"]?.borderSize;
|
||||
const hexSizeSupportBorderSize = token.document.flags["hex-size-support"]?.borderSize;
|
||||
if (hexSizeSupportBorderSize > 0) {
|
||||
w = h = hexSizeSupportBorderSize
|
||||
}
|
||||
else {
|
||||
w = token.data.width
|
||||
h = token.data.height
|
||||
w = h = hexSizeSupportBorderSize;
|
||||
} else {
|
||||
w = token.document.width;
|
||||
h = token.document.height;
|
||||
}
|
||||
return {w, h};
|
||||
}
|
||||
@@ -203,7 +227,7 @@ export function getTokenSize(token) {
|
||||
// This function applies an offset to to the waypoints that will move the ruler from the edge to the center of the cell
|
||||
export function applyTokenSizeOffset(waypoints, token) {
|
||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||
return waypoints
|
||||
return waypoints;
|
||||
}
|
||||
|
||||
const tokenSize = getTokenSize(token);
|
||||
@@ -214,21 +238,17 @@ export function applyTokenSizeOffset(waypoints, token) {
|
||||
if (canvas.grid.grid.options.columns) {
|
||||
if (tokenSize.w % 2 === 0) {
|
||||
waypointOffset.x = canvas.grid.w / 2;
|
||||
if (!isAltOrientation)
|
||||
waypointOffset.x *= -1;
|
||||
if (!isAltOrientation) waypointOffset.x *= -1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (tokenSize.h % 2 === 0) {
|
||||
waypointOffset.y = canvas.grid.h / 2;
|
||||
if (isAltOrientation)
|
||||
waypointOffset.y *= -1;
|
||||
if (isAltOrientation) waypointOffset.y *= -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If hex size support isn't active leave the waypoints like they are
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (tokenSize.w % 2 === 0) {
|
||||
waypointOffset.x = canvas.grid.w / 2;
|
||||
}
|
||||
@@ -237,7 +257,7 @@ export function applyTokenSizeOffset(waypoints, token) {
|
||||
}
|
||||
}
|
||||
|
||||
return waypoints.map(w => new PIXI.Point(w.x + waypointOffset.x, w.y + waypointOffset.y))
|
||||
return waypoints.map(w => new PIXI.Point(w.x + waypointOffset.x, w.y + waypointOffset.y));
|
||||
}
|
||||
|
||||
export function setSnapParameterOnOptions(sourceObject, options) {
|
||||
@@ -246,8 +266,7 @@ export function setSnapParameterOnOptions(sourceObject, options) {
|
||||
options.snapOverrideActive = true;
|
||||
options.snap = sourceObject.snapOverride.snap;
|
||||
sourceObject.snapOverride = undefined; // remove it to prevent any lingering data issues
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
options.snap = !disableSnap;
|
||||
}
|
||||
}
|
||||
@@ -257,8 +276,29 @@ export function isClose(a, b, delta) {
|
||||
}
|
||||
|
||||
export function getMeasurePosition() {
|
||||
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens);
|
||||
const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(
|
||||
canvas.tokens,
|
||||
);
|
||||
const rulerOffset = canvas.controls.ruler.rulerOffset;
|
||||
const measurePosition = {x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y};
|
||||
return measurePosition;
|
||||
}
|
||||
|
||||
// isGM function for use during loading when game.user isn't available yet
|
||||
export function early_isGM() {
|
||||
const level = game.data.users.find(u => u._id == game.data.userId).role;
|
||||
const gmLevel = CONST.USER_ROLES.ASSISTANT;
|
||||
return level >= gmLevel;
|
||||
}
|
||||
|
||||
export function isModuleActive(moduleName) {
|
||||
return game.modules.get(moduleName)?.active;
|
||||
}
|
||||
|
||||
export function isPathfindingEnabled() {
|
||||
if (!window.routinglib) return false;
|
||||
if (this.user !== game.user) return false;
|
||||
if (!game.user.isGM && !game.settings.get(settingsKey, "allowPathfinding")) return false;
|
||||
if (moveWithoutAnimation) return false;
|
||||
return game.settings.get(settingsKey, "autoPathfinding") != togglePathfinding;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user